7.5. Flyweight

  • EN: Flyweight

  • PL: Pyłek

  • Type: object

7.5.1. Pattern

  • In applications with large number of objects

  • Objects take significant amount of memory

  • Reduce memory consumed by objects

../../_images/designpatterns-flyweight-pattern.png

7.5.2. Problem

  • Imagine mapping application, such as: Open Street Maps, Google Maps, Yelp, Trip Advisor etc.

  • There are thousands points of interests such as Cafe, Shops, Restaurants, School etc.

  • Icons can take a lot of memory, times number of points on the map

  • It might crash with out of memory error (especially on mobile devices)

design-patterns/structural/img/designpatterns-flyweight-problem.png

from dataclasses import dataclass
from enum import Enum


class PointType(Enum):
    HOSPITAL = 1
    CAFE = 2
    RESTAURANT = 3


@dataclass
class Point:
    x: int            # 28 bytes
    y: int            # 28 bytes
    type: PointType   # 1064 bytes
    icon: bytearray   # empty: 56 bytes, but with PNG icon: 20 KB

    def draw(self) -> None:
        print(f'{self.type} at ({self.x}, {self.y})')


class PointService:
    def get_points(self) -> list[Point]:
        points: list[Point] = list()
        point: Point = Point(1, 2, PointType.CAFE, None)
        points.append(point)
        return points


if __name__ == '__main__':
    service = PointService()
    for point in service.get_points():
        point.draw()
        # PointType.CAFE at (1, 2)

7.5.3. Solution

  • Separate the data you want to share from other data

  • Pattern will create a dict with point type and its icon

  • It will reuse icon for each type

  • So it will prevent from storing duplicated data in memory

../../_images/designpatterns-flyweight-solution.png

from dataclasses import dataclass, field
from enum import Enum
from typing import Final


class PointType(Enum):
    HOSPITAL = 1
    CAFE = 2
    RESTAURANT = 3


@dataclass
class PointIcon:
    type: Final[PointType]   # 1064 bytes
    icon: Final[bytearray]   # empty: 56 bytes, but with PNG icon: 20 KB

    def get_type(self):
        return self.type


@dataclass
class PointIconFactory:
    icons: dict[PointType, PointIcon] = field(default_factory=dict)

    def get_point_icon(self, type: PointType) -> PointIcon:
        if not self.icons.get(type):
            self.icons[type] = PointIcon(type, None)  # Here read icon from filesystem
        return self.icons.get(type)


@dataclass
class Point:
    x: int  # 28 bytes
    y: int  # 28 bytes
    icon: PointIcon

    def draw(self) -> None:
        print(f'{self.icon.get_type()} at ({self.x}, {self.y})')


@dataclass
class PointService:
    icon_factory: PointIconFactory

    def get_points(self) -> list[Point]:
        points: list[Point] = list()
        point: Point = Point(1, 2, self.icon_factory.get_point_icon(PointType.CAFE))
        points.append(point)
        return points


if __name__ == '__main__':
    service = PointService(PointIconFactory())
    for point in service.get_points():
        point.draw()
        # PointType.CAFE at (1, 2)

7.5.4. Assignments