Паттерн Приспособленец в Python
Паттерн Легковес — это структурный паттерн проектирования, который минимизирует использование памяти и повышает производительность за счёт совместного использования объектов. Проще говоря, это идея «совместного использования метаданных».
Представьте, что вы берёте книги в библиотеке: если бы каждую книгу покупали отдельно разные люди, библиотеке пришлось бы хранить тысячи одинаковых книг. Но в действительности библиотеке достаточно хранить один экземпляр «Введения в Python», и каждый, кто хочет её прочитать, может взять тот же самый экземпляр. Паттерн Легковес — это такая «библиотека», которая управляет объектами, доступными для совместного использования.
Ключевая идея
Паттерн Легковес разделяет состояние объектов на два типа:
- Внутреннее состояние: неизменяемая, разделяемая часть
- Внешнее состояние: изменяемая, неразделяемая часть
За счет совместного использования внутреннего состояния удается избежать создания большого количества похожих объектов, тем самым экономя системные ресурсы.
Зачем нужен паттерн Легковес
Сценарий проблемы
Предположим, мы разрабатываем текстовый процессор и нам нужно отображать символы в документе. Если мы создаём отдельный объект для каждого символа:
Пример
class Character:
def __init__(self, char, font, size, color):
self.char = char # символ
self.font = font # шрифт
self.size = size # размер шрифта
self.color = color # цвет
def render(self, position):
print(f"Отобразить символ '{self.char}' в позиции {position}")
# пример использования
char_a = Character('A', 'SimSun', 12, 'black')
char_b = Character('B', 'SimSun', 12, 'black')
char_a_another = Character('A', 'SimSun', 12, 'black') # многократное создание одной и той же 'A'
Проблемы с этой реализацией:
- Бесполезный расход памяти: одни и те же символы создаются многократно
- Плохая производительность: накладные расходы на создание и уничтожение большого количества объектов
- Сложно поддерживать: число объектов стремительно растёт
Решение
Используя паттерн Легковес, мы можем:
- Разделять одни и те же объекты символов
- Хранить только одну копию внутреннего состояния (сам символ)
- Передавать внешнее состояние (позицию) при отрисовке
Реализация паттерна Легковес
Базовая структура
Давайте реализуем паттерн Легковес на примере текстового процессора:
Пример
# Класс Легковес - хранит внутреннее состояние
class CharacterFlyweight:
def __init__(self, char: str, font: str, size: int, color: str):
self.char = char # Внутреннее состояние: содержимое символа
self.font = font # Внутреннее состояние: шрифт
self.size = size # Внутреннее состояние: размер шрифта
self.color = color # Внутреннее состояние: цвет
def render(self, position: tuple):
"""Отрисовать символ; position — это внешнее состояние"""
x, y = position
print(f"Отрисовать в ({x}, {y}): символ '{self.char}' "
f"[шрифт:{self.font}, размер:{self.size}, цвет:{self.color}]")
# Фабрика легковесов - управляет разделяемыми объектами
class CharacterFactory:
_characters: Dict[str, CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
# Уникальный идентификационный ключ для создания объектов
key = f"{char}_{font}_{size}_{color}"
# Если объект не существует, создайте и закэшируйте его
if key not in cls._characters:
cls._characters[key] = CharacterFlyweight(char, font, size, color)
print(f"Создание нового объекта символа: {key}")
else:
print(f"Повторное использование существующего объекта символа: {key}")
return cls._characters[key]
# Клиентский класс — использует объекты-приспособленцы
class TextDocument:
def __init__(self):
self.characters = [] # Хранит информацию о символах и позициях
def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
# Получает объект-приспособленец из фабрики
character = CharacterFactory.get_character(char, font, size, color)
# Сохраняет объект символа и внешнее состояние (позицию)
self.characters.append((character, position))
def render(self):
print("\n"=== Начало отрисовки документа ===")
for character, position in self.characters:
character.render(position)
print("=== Отрисовка документа завершена ==="\n")
Пример использования
Пример
document = TextDocument()
# Добавить символ в документ
document.add_character('H', 'Arial', 12, 'black', (0, 0))
document.add_character('e', 'Arial', 12, 'black', (1, 0))
document.add_character('l', 'Arial', 12, 'black', (2, 0))
document.add_character('l', 'Arial', 12, 'black', (3, 0)) # Повторно использовать 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0)) # Другой цвет, создать новый объект
document.add_character('H', 'Arial', 12, 'black', (0, 1)) # Повторно использовать 'H'
# Отрисовать документ
document.render()
# Просмотреть количество объектов на фабрике
print(f"Фабрика создала всего {len(CharacterFactory._characters)} объектов-символов")
Результат вывода:
创建新字符对象: H_Arial_12_black 创建新字符对象: e_Arial_12_black 创建新字符对象: l_Arial_12_black 重用现有字符对象: l_Arial_12_black 创建新字符对象: o_Arial_12_black 创建新字符对象: !_Arial_12_red 重用现有字符对象: H_Arial_12_black === 开始渲染文档 === 在位置(0, 0)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black] 在位置(1, 0)渲染: 字符'e' [字体:Arial, 大小:12, 颜色:black] 在位置(2, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black] 在位置(3, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black] 在位置(4, 0)渲染: 字符'o' [字体:Arial, 大小:12, 颜色:black] 在位置(5, 0)渲染: 字符'!' [字体:Arial, 大小:12, 颜色:red] 在位置(0, 1)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black] === 文档渲染完成 === 工厂中总共创建了 6 个字符对象
Основные компоненты паттерна Легковес
1. Легковес (интерфейс или абстрактный класс Легковеса)
Определяет интерфейс для объектов-легковесов, обычно содержащий методы для работы с внешним состоянием.
2. ConcreteFlyweight (Конкретный класс-легковес)
Реализует интерфейс легковеса и хранит внутреннее состояние. Внутреннее состояние должно быть неизменяемым.
3. FlyweightFactory (Фабрика легковесов)
Создаёт объекты-легковесы и управляет ими, обеспечивая корректное совместное использование объектов-легковесов.
4. Клиент
Хранит внешнее состояние и запрашивает объекты-легковесы у фабрики легковесов при необходимости.
Более сложный пример: применение в разработке игр
Рассмотрим практический пример из разработки игр — систему рендеринга деревьев:
Пример
from dataclasses import dataclass
@dataclass
class TreeType:
"""Класс-легковес — тип дерева (внутреннее состояние)"""
name: str # Название вида дерева
texture: str # Файл текстуры
color: str # Базовый цвет
def render(self, x: int, y: int, height: int):
"""Отрисовать дерево; позиция и высота — внешнее состояние"""
print(f"Отрисовать дерево {self.name} в ({x}, {y}), высота {height} метров "
f"[texture:{self.texture}, color:{self.color}]")
class TreeFactory:
"""Фабрика легковесов — управляет типами деревьев"""
_tree_types: Dict[str, TreeType] = {}
@classmethod
def get_tree_type(cls, name: str, texture: str, color: str) -> TreeType:
key = f"{name}_{texture}_{color}"
if key not in cls._tree_types:
cls._tree_types[key] = TreeType(name, texture, color)
print(f"Создание нового типа дерева: {name}")
return cls._tree_types[key]
@classmethod
def list_tree_types(cls):
print(f"\n"В настоящее время существует {len(cls._tree_types)} типов деревьев:")
for tree_type in cls._tree_types.values():
print(f" - {tree_type.name}")
class Tree:
"""Объект Tree — содержит ссылку на легковес и внешнее состояние"""
def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
self.x = x # Внешнее состояние: координата X
self.y = y # Внешнее состояние: координата Y
self.height = height # Внешнее состояние: height
self.tree_type = tree_type # Ссылка на объект-легковес
def render(self):
self.tree_type.render(self.x, self.y, self.height)
class Forest:
"""Forest - клиентский класс"""
def __init__(self):
self.trees: List[Tree] = []
def plant_tree(self, x: int, y: int, height: int,
name: str, texture: str, color: str):
tree_type = TreeFactory.get_tree_type(name, texture, color)
tree = Tree(x, y, height, tree_type)
self.trees.append(tree)
def render(self):
print("\n"=== Начинаем рендеринг леса ===")
for tree in self.trees:
tree.render()
print("=== Рендеринг леса завершён ===")
# Пример использования
forest = Forest()
# Сажаем деревья - деревья одного типа совместно используют легковесные объекты
forest.plant_tree(10, 20, 15, "Сосна", "pine_texture.png", "Тёмно-зелёный")
forest.plant_tree(30, 40, 12, "Сосна", "pine_texture.png", "Тёмно-зелёный") # Повторно использовать тип сосны
forest.plant_tree(50, 60, 18, "Дуб", "oak_texture.png", "Светло-зелёный")
forest.plant_tree(70, 80, 20, "Сосна", "pine_texture.png", "Тёмно-зелёный") # Повторно использовать
forest.plant_tree(90, 100, 16, "Клён", "maple_texture.png", "Красный")
# Отрисовать лес
forest.render()
# Просмотр статистики типов деревьев
TreeFactory.list_tree_types()
Плюсы и минусы паттерна Легковес
Преимущества
- Значительно снижает использование памяти: значительно снижает потребление памяти за счёт совместного использования похожих объектов
- Повышает производительность: снижает накладные расходы на создание объектов и сборку мусора
- Повторное использование кода: одну и ту же логику объекта нужно реализовать только один раз
- Легко расширять: добавление новых типов легковесов не влияет на существующий код
Недостатки
- Увеличивает сложность: нужно различать внутреннее и внешнее состояние
- Проблемы потокобезопасности: разделяемые объекты требуют дополнительной обработки в многопоточных средах
- Может приводить к ошибкам: если внутреннее состояние будет ошибочно изменено, это повлияет на всех пользователей
- Применимо не во всех сценариях: Эффективно только тогда, когда объект действительно может быть разделён
Применимые сценарии
Сценарии, подходящие для паттерна Приспособленец
- Большое количество похожих объектов: Когда системе нужно создавать большое количество похожих объектов
- Приложения, чувствительные к памяти: Среды с ограниченной памятью, такие как мобильные устройства и встраиваемые системы
- Системы кэширования: Когда объекты нужно кэшировать и повторно использовать
- Разработка игр: Рендеринг большого количества игровых объектов одного типа
- Обработка документов: Текстовые процессоры, электронные таблицы и т. д.
Сценарии, не подходящие для использования
- Объекты сильно различаются: Если каждый объект имеет уникальное состояние
- Сложное внешнее состояние: Если управление внешним состоянием сложнее, чем создание объекта
- Низкие требования к производительности: В сценариях с достаточным объёмом памяти и низкими требованиями к производительности
Практические упражнения
Упражнение 1: Улучшите систему рендеринга символов
Попробуйте улучшить нашу предыдущую систему рендеринга символов, добавив поддержку таких стилей, как полужирный и курсив:
Пример
class AdvancedCharacterFlyweight:
# Добавьте поддержку полужирного, курсива и подчеркивания
pass
# Протестируйте свою реализацию
def test_advanced_system():
# Создайте документ, содержащий различные стили
pass
Упражнение 2: Реализуйте систему управления иконками
Разработайте систему управления иконками, в которой одна и та же иконка использует один и тот же объект при отображении в разных позициях:
Пример
# Хранить внутреннее состояние, например путь к файлу значка и размеры
pass
class IconFactory:
# Управление объектами-легковесами иконок
pass
class Application:
# Отображение иконок в разных позициях на интерфейсе
pass
Итог
Паттерн Легковес — мощная техника оптимизации, которая снижает потребление ресурсов за счёт совместного использования объектов. Ключевые моменты:
- Различайте состояния: Чётко отличайте внутреннее состояние (разделяемое) от внешнего состояния (неразделяемого)
- Используйте фабрику: Управляйте созданием и совместным использованием объектов-легковесов через класс фабрики
- Взвесьте плюсы и минусы: Найдите баланс между экономией памяти и сложностью кода
- Применимые сценарии: В основном используется в сценариях с большим количеством похожих объектов