handbookНаписать

patterns/structural/flyweight/README.md · тема · 4 языка · 15 тестов

+ практика: стили ячеек таблицы

Приспособленец (Flyweight) ​

Много мелких объектов делят одно неизменяемое общее состояние, а уникальное хранят у себя или получают параметрами.

Проблема ​

Игра рисует лес на сто тысяч деревьев. Каждое дерево — объект с координатами, а также названием породы, цветом и текстурой. Координаты у всех разные, а пород три: дуб, берёза, ель. Если каждое дерево хранит свою копию текстуры (в игре это мегабайты, а не строка rough), память кончается задолго до того, как лес становится лесом.

Наивное «вынести породу в отдельный объект» не помогает, если каждый вызов new создаёт новый: сто тысяч одинаковых пород всё равно лежат в памяти. Нужно, чтобы одинаковые породы были одним объектом и чтобы этот объект никто не мог поменять из-под соседей.

Решение ​

Состояние дерева делится на два сорта. Внутреннее (intrinsic) — то, что одинаково у многих: порода, цвет, текстура. Оно уходит в TreeType — неизменяемый объект, который делят все деревья этой породы. Внешнее (extrinsic) — то, что у каждого своё: координаты. Оно остаётся в дереве или передаётся параметром в метод приспособленца: draw(x, y).

Приспособленец обязан быть неизменяемым. Если дуб номер 17 перекрасит свою породу в жёлтый, пожелтеют все дубы: общий объект нельзя править, можно только взять другой.

Структура ​

thousands

one per key

shared

TreeType

+name: string

+color: string

+texture: string

+draw(x, y) : string

TreeTypes

-types: Map<key, TreeType>

+get(name, color, texture) : TreeType

+count() : int

Tree

+x: int

+y: int

+type: TreeType

+draw() : string

Forest

-trees: Tree[]

+plant(x, y, name, color, texture)

+draw() : string[]

+typeCount() : int

Контекст — это Tree и Forest. Дерево хранит только своё: координаты и ссылку на породу; рисуя себя, оно передаёт координаты породе. Лес не создаёт породы сам, а просит их у фабрики, поэтому тысяча посаженных дубов получает одну и ту же ссылку.

Как это собирается ​

Деревья сажает загрузчик уровня. В примере уровень - это легенда (символ карты -> порода, цвет, текстура) и сама карта, где каждая клетка с символом - одно дерево. Загрузчик идёт по клеткам и зовёт plant() с координатами клетки и данными породы из легенды, а Forest через фабрику TreeTypes отдаёт дереву общий TreeType. Сколько бы дубов ни было на карте, текстура дуба загружается один раз.

Неизвестный символ на карте - ошибка в файле уровня, но не повод не грузить уровень: загрузчик пропускает клетку и пишет её координаты. В конце видно главное: четыре дерева и два типа. Координаты остались в деревьях, всё общее ушло в приспособленцев.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Фабрика TreeTypes — кэш по ключу из внутреннего состояния: если порода с таким ключом уже есть, возвращается она, иначе создаётся и запоминается. Без фабрики приспособленец не работает: именно она гарантирует «одинаковое = тот же объект». Счётчик созданных пород нужен тестам: он показывает экономию числом, без замера памяти.

ЯзыкОсобенность
PHPreadonly class для породы; кэш — массив с ключом-строкой и ??=
GoСравнимая структура сама служит ключом карты; неизменяемость — через неэкспортируемые поля
Python@dataclass(frozen=True, slots=True): неизменяемость и экономия на __dict__
TypeScriptreadonly поля плюс Object.freeze; ключ — строка, потому что Map сравнивает объекты по ссылке

Запуск: make test T=patterns/structural/flyweight

Плюсы и минусы ​

Тесты проверяют три вещи. Тысяча деревьев двух пород создаёт ровно две породы: экономия видна по счётчику фабрики. Одинаковое внутреннее состояние даёт тот же объект, а отличие хотя бы в одном поле — другой. Внешнее состояние доходит до приспособленца параметром: два дуба в разных точках рисуются по-разному одной и той же породой.

  • ✅ Память растёт с числом уникальных сочетаний, а не с числом объектов.
  • ✅ Общие данные загружаются и проверяются один раз (текстура, шрифт, глиф).
  • ✅ Неизменяемость приспособленца даёт бесплатную потокобезопасность: делить можно без блокировок.
  • ❌ Состояние объекта размазано на два места: часть в нём, часть приходит параметрами. Код читается хуже.
  • ❌ Фабрика — кэш, а у кэша свои вопросы: кто его очищает, сколько он живёт, что если уникальных ключей вдруг миллион.
  • ❌ Менять приспособленца нельзя, только заменить ссылку на другой: изменение «одного дерева» превращается в поиск или создание новой породы.

Когда не применять ​

  • Объектов немного, и память не проблема: два класса и фабрика вместо одного — лишняя сложность.
  • Общей части почти нет: если у каждого объекта свой цвет и своя текстура, кэш будет расти вместе с объектами и только добавит накладных расходов.
  • Общее состояние должно меняться у части объектов: приспособленец неизменяем по определению, тут нужна ссылка на изменяемый общий объект, и это уже другой разговор.

Отличия между языками ​

  • PHP — readonly class делает неизменяемость гарантией языка, а ??= укладывает «найти или создать» в одну строку. Ключ собирается строкой: массив PHP не умеет ключ-объект.
  • Go — структура из строк сравнима, поэтому она сама становится ключом map[TreeType]*TreeType: отдельный ключ собирать не нужно. Делится указатель, а неизменяемость держится на неэкспортируемых полях: вне пакета их не поменять.
  • Python — frozen=True запрещает присваивание и даёт __hash__, slots=True убирает __dict__ у каждого экземпляра, что для тысяч мелких объектов само по себе заметная экономия. Кэш можно было бы сделать functools.cache на функции-фабрике, но тогда он глобальный и его не посчитать в тесте.
  • TypeScript — readonly проверяется только компилятором, Object.freeze запрещает запись и в рантайме. Map сравнивает объекты по ссылке, поэтому ключ — строка из полей. Одинаковые строковые литералы движок и так хранит один раз, поэтому приспособленец имеет смысл для объектов, а не для отдельных строк.

Практика ​

Учебный лес создаётся один раз и больше не меняется. В табличном редакторе стили ячеек меняются постоянно, и вопрос «как изменить общий неизменяемый объект для одной ячейки» становится главным. Практика: стили ячеек таблицы показывает пул стилей на 10 000 ячеек, копирование при записи и то, как это устроено в файле .xlsx.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS