7.5. Flyweight

  • EN: Flyweight

  • PL: Pyłek

  • Type: object

The Flyweight design pattern is a structural design pattern that allows you to fit more objects into the available amount of RAM by sharing common parts of state between multiple objects, instead of keeping all of the data in each object.

Here's a simple example of the Flyweight pattern in Python:

>>> class Flyweight:
...     def __init__(self, shared_state: str) -> None:
...         self._shared_state = shared_state
...
...     def operation(self, unique_state: str) -> None:
...         print(f"Flyweight: Displaying shared ({self._shared_state}) and unique ({unique_state}) state.")
...
>>> class FlyweightFactory:
...     _flyweights = {}
...
...     def get_flyweight(self, shared_state: str) -> Flyweight:
...         if not self._flyweights.get(shared_state):
...             self._flyweights[shared_state] = Flyweight(shared_state)
...         return self._flyweights[shared_state]
...
>>> factory = FlyweightFactory()
>>> flyweight = factory.get_flyweight("shared state")
>>> flyweight.operation("unique state")
Flyweight: Displaying shared (shared state) and unique (unique state) state.

In this example, Flyweight is a class whose instances with the same state can be used interchangeably. FlyweightFactory is a class that creates and manages the Flyweight objects. It ensures that flyweights are shared correctly. When a client requests a flyweight, the FlyweightFactory objects supply an existing instance or create a new one, if it doesn't exist yet.

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