9.5. Flyweight

9.5.1. Rationale

  • EN: Flyweight

  • PL: Pyłek

  • Type: object

9.5.2. Use Cases

  • In applications with large number of objects

  • Objects take significant amount of memory

  • Reduce memory consumed by objects

9.5.3. 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)

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)

9.5.4. Design

../../_images/designpatterns-flyweight-gof.png

9.5.5. Implementation

  • 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-usecase.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)

9.5.6. Assignments