Паттерн Flyweight (Приспособленец)
Паттерн Flyweight (англ. Flyweight, произносится как «флайвейт») — это структурный паттерн проектирования, который позволяет вмещать больше объектов в отведенную оперативную память за счет разделения общего состояния между несколькими объектами, вместо хранения одинаковых данных в каждом из них.
Подробное описание
Паттерн применяется, когда в приложении необходимо создать огромное количество однотипных объектов, что приводит к исчерпанию доступной памяти. Ключевая идея заключается в разделении состояния объекта на две части:
- Внутреннее состояние (intrinsic) — данные, которые не зависят от контекста и могут быть разделены между множеством объектов (например, тип символа, текстура дерева, цвет кнопки).
- Внешнее состояние (extrinsic) — данные, которые уникальны для каждого конкретного экземпляра и передаются объекту извне при использовании (например, координаты символа на экране, позиция дерева в мире, состояние нажатия кнопки).
Flyweight-фабрика управляет пулом уже созданных объектов. Если запрашивается объект с определенным внутренним состоянием, фабрика либо возвращает существующий экземпляр, либо создает новый. Это гарантирует, что объекты с одинаковым внутренним состоянием будут представлены единственным экземпляром в памяти.
Основные принципы
Математическая оценка эффективности
Эффективность паттерна можно оценить через соотношение потребляемой памяти до и после применения оптимизации.
Пусть \(N\) — общее количество объектов, \(K\) — количество уникальных комбинаций внутреннего состояния (\(K \ll N\)).
Память без паттерна:
Память с паттерном Flyweight:
Где:
- \(S_{intrinsic}\) — размер внутреннего состояния (разделяемого).
- \(S_{extrinsic}\) — размер внешнего состояния (уникального).
- \(S_{factory}\) — накладные расходы на хранение фабрики и ссылок.
Выгода достигается, когда \(N \times S_{intrinsic} \gg K \times S_{intrinsic} + S_{factory}\).
Блок-схема взаимодействия
sequenceDiagram
participant Client as Клиент
participant Factory as FlyweightFactory
participant Flyweight as ConcreteFlyweight
Client->>Factory: getFlyweight(key)
alt Объект существует в пуле
Factory-->>Client: Вернуть существующий объект
else Объекта нет в пуле
Factory->>Flyweight: Создать новый объект
Flyweight-->>Factory: Новый объект
Factory-->>Client: Вернуть новый объект
end
Client->>Flyweight: operation(extrinsicState)
Пример реализации на Python
Рассмотрим реализацию текстового редактора. Вместо создания отдельного объекта для каждой буквы «а» в документе (которых могут быть тысячи), мы создаем один разделяемый объект для символа «а», хранящий его шрифт и начертание (внутреннее состояние). Координаты и цвет конкретной буквы передаются при отрисовке (внешнее состояние).
import weakref
from typing import Dict, Tuple
class CharacterFlyweight:
"""
Легковесный объект, хранящий внутреннее состояние символа.
"""
def __init__(self, char: str, font: str, size: int):
self.char = char # Внутреннее состояние
self.font = font # Внутреннее состояние
self.size = size # Внутреннее состояние
def render(self, x: int, y: int, color: str):
"""
Отрисовка символа. Координаты и цвет - внешнее состояние.
"""
print(f"Отрисовка '{self.char}' [Шрифт: {self.font}, Размер: {self.size}] "
f"в позиции ({x}, {y}) цветом {color}")
class CharacterFactory:
"""
Фабрика для управления пулом flyweight-объектов.
Использует WeakValueDictionary, чтобы объекты удалялись сборщиком мусора,
если на них больше нет ссылок вне фабрики (опционально для долгосрочных приложений).
"""
_pool: Dict[Tuple[str, str, int], CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int) -> CharacterFlyweight:
key = (char, font, size)
if key not in cls._pool:
cls._pool[key] = CharacterFlyweight(char, font, size)
return cls._pool[key]
@classmethod
def get_pool_size(cls) -> int:
return len(cls._pool)
class TextEditor:
"""
Клиентский код, хранящий внешний контекст (позиции символов).
"""
def __init__(self):
self.characters: list[Tuple[CharacterFlyweight, int, int, str]] = []
def add_character(self, char: str, font: str, size: int, x: int, y: int, color: str):
# Получаем разделяемый объект из фабрики
flyweight = CharacterFactory.get_character(char, font, size)
# Сохраняем ссылку на flyweight вместе с внешним состоянием
self.characters.append((flyweight, x, y, color))
def render(self):
for flyweight, x, y, color in self.characters:
flyweight.render(x, y, color)
if __name__ == "__main__":
editor = TextEditor()
# Добавляем много текста с повторяющимися стилями
text_content = "Hello World! Hello Python!"
x_pos = 0
for char in text_content:
if char.isupper():
font, size, color = "Arial Bold", 14, "red"
elif char == ' ':
font, size, color = "Arial", 12, "white" # Пробел тоже объект, но невидимый
else:
font, size, color = "Arial", 12, "black"
editor.add_character(char, font, size, x_pos, 10, color)
x_pos += 10
print(f"Всего символов в тексте: {len(text_content)}")
print(f"Уникальных объектов Flyweight в памяти: {CharacterFactory.get_pool_size()}")
print("-" * 30)
editor.render()
Достоинства и недостатки
Достоинства:
- Экономия памяти. Значительное снижение потребления RAM при работе с большими наборами однотипных данных.
- Производительность. Ускорение инициализации, так как объекты не создаются каждый раз заново, а берутся из кэша.
- Централизованное управление. Изменение внутреннего состояния (например, глобальная замена шрифта) происходит в одном месте для всех связанных объектов.
Недостатки:
- Усложнение кода. Необходимо четко разделять внутреннее и внешнее состояние, что увеличивает сложность архитектуры.
- Накладные расходы на вычисление хэшей. Поиск объектов в фабрике требует времени, хотя обычно оно компенсируется экономией на создании объектов.
- Проблемы с многопоточностью. Если разделяемые объекты изменяемы, требуется синхронизация доступа, что может снизить производительность. Рекомендуется делать Flyweight-объекты неизменяемыми (immutable).
Области применения
- Паттерны проектирования (реализация эффективных структур данных, кэширование)
- Игровая разработка (отрисовка тысяч деревьев, травы, камней с одинаковыми текстурами но разными координатами)
- Обработка естественного языка (хранение словаря слов в текстовых процессорах, где каждое слово — объект с общим форматированием)