Также известен как Приспособленец, Кэш, Flyweight

Легковес

Суть паттерна

Если у разных объектов совпадает часть состояния, Легковес позволяет им разделять между собой это состояние, не храня его в каждом объекте. Это экономит память и позволяет программе иметь огромное количество похожих объектов.

Проблема

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

Например, вы делаете видеоигру и в ней вам нужно отрисовать лес. Вы создаёте для каждого дерева объект, имеющий данные о координатах, типе дерева, цвете, 3D модели и текстуре. Все работает отлично для небольшой рощицы, но как только вы увеличиваете масштаб, создаются тысячи новых объектов деревьев. А значит, вскоре заканчивается оперативная память и в работу включается медленный файл подкачки на жёстком диске, который окончательно убивает производительность программы.

Решение

Посмотрите внимательно на ваш лес объектов. Все дубы в лесу отличаются только позицией, но имеют тот же тип, цвет, модель и текстуру. Паттерн Легковес предлагает не хранить одинаковые данные в каждом объекте, а ссылаться на них, или передавать через параметры методов. Из-за этого их принято называть «внешним состоянием». Все что останется внутри класса Легковеса будет называться внутренним состоянием.

Проделав это, вы можете радикально уменьшить количество уникальных объектов в программе. В нашем лесном примере, у вас останется всего 3 объекта — дуб, берёза и сосна. Карта позиций деревьев может храниться или высчитываться на клиенте и передаваться в объект дерева при его отрисовке.

Остаётся вопрос создания новых объектов-легковесов. Когда нужно создавать новый Легковес, а когда можно использовать существующий?

Обычно, эта логика живёт в дополнительном фабричном классе и не загромождает код клиентов легковеса.

Другими словами, раньше клиенты могли создавать объекты легковесов напрямую, назначения им какое-то состояние. Теперь, клиент «звонит» на фабрику и передаёт ей внутреннее состояние желаемого объекта. В свою очередь, фабрика либо возвращает существующий объект с таким же состоянием, либо создаёт новый.

Я называю этот паттерн «Легковесом», по двум причинам. Во-первых, это название ближе к оригинальному названию «Flyweight». Во-вторых, оно сильнее отражает суть паттерна — наличие легковесных объектов.

Однако знайте, что паттерн Flyweight встречается в русской литературе под названием Приспособленец. Это название не совсем удачное и, как мне кажется, требует пояснения. Паттерн назвали Приспособленцем потому, что он может быть одновременно использован в разных контекстах. Он словно приспосабливается к контексту своего использования. У клиента при этом создаётся впечатление, что он работает с разными объектами. Хотя, на самом деле это один и тот же объект.

Структура

Схема структуры классов паттерна Легковес (Приспособленец)
  1. Легковес задаёт интерфейс, с помощью которого конкретные легковесы могут получать внешнее состояние и как-то воздействовать на него.

  2. Конкретный легковес реализует интерфейс Легковеса и содержит собственное внутреннее состояние, не зависящее от контекста, где используется объект.

    Одни и те же объекты конкретных легковесов могут быть использованы в различных контекстах так, как будто это разные объекты.

  3. Неразделяемый легковес хоть и реализует общий интерфейс Легковеса, но сохраняет в себе полное состояние.

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

    Благодаря общему интерфейсу, их можно использовать совместно с конкретными легковесами.

  4. Фабрика легковесов создаёт объекты-легковесы и управляет ними.

    Она отвечает за повторное использование существующих объектов-легковесов. Когда Клиент запрашивает легковеса, Фабрика либо отдаёт существующий экземпляр, либо создаёт новый, если такового ещё не было создано.

    Клиент передаёт Фабрике значения внутреннего состояния желаемого легковеса. Эти данные используются при поиске объектов в индексе, а также для инициализации новых легковесов.

  5. Клиент — получает нужные объекты легковесов из Фабрики.

    Вычисляет или хранит контекст — внешнее состояние легковесов. Легковес получает контекст через параметры своих методов.

Псевдокод

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

Таким образом, паттерн Легковес позволяет здорово экономить память в случаях, когда вы имеете дело с миллионами тяжёлых, но слегка похожих друг на друга объектов.

// Этот класс-легковес содержит часть полей, которые описывают деревья. Эти поля
// не уникальные для каждого дерева как, например, координаты. Например,
// несколько деревьев могут иметь ту же текстуру. Поэтому мы переносим
// повторяющиеся данные в один единственный объект и ссылаемся на него из
// конкретных деревьев.
class TreeType is
    field name
    field color
    field texture
    constructor Tree(name, color, texture) { ... }
    method draw(canvas, x, y) is
        Create a bitmap from type, color and texture.
        Draw bitmap on canvas at X and Y.

// Фабрика легковесов решает когда нужно создать новый легковес, а когда можно
// обойтись существующим.
class TreeFactory is
    static field treeTypes: collection of tree types
    static method getTreeType(name, color, texture) is
        result = treeTypes.find(name, color, texture)
        if (result == null)
            tree = new TreeType(name, color, texture)
            treeTypes.add(tree)
        return tree

// Контекстный объект, из которого мы выделили легковес TreeType. В программе
// могут быть тысячи объектов Tree, так как накладные расходы на их хранение
// совсем небольшие — порядка трёх целых чисел (две координаты и ссылка).
class Tree is
    field x,y
    field type: TreeType
    constructor Tree(x, y, type) { ... }
    method draw(canvas) is
        type.draw(canvas, this.x, this.y)

// Классы Tree и Forest являются клиентами легковеса. При желании их можно слить
// в один класс, если класс Tree нет нужды расширять далее.
class Forest is
    field trees: collection of Trees

    method plantTree(x, y, name, color, texture) is
        type = TreeFactory.getTreeType(name, color, texture)
        tree = new Tree(x, y, type);
        trees.add(tree)

    method draw(canvas) is
        foreach tree in trees
            tree.draw(canvas)

Применимость

Экономия системных ресурсов.

Эффективность паттерна Легковес во многом зависит от того, как и где он используется. Применяйте этот паттерн, когда выполнены все перечисленные условия:

  • в приложении используется большое число объектов;
  • из-за этого высоки расходы оперативной памяти;
  • большую часть состояния объектов можно вынести за пределы их классов;
  • многие группы объектов можно заменить относительно небольшим количеством разделяемых объектов, поскольку внешнее состояние вынесено;
  • приложение не зависит от идентичности объектов. Проверка на идентичность возвратит «истину» для различных объектов, представленных одним реальным легковесом.

Шаги реализации

  1. Убедитесь что соблюдены условия применения Легковеса:

    • Программа ест слишком много памяти.
    • Большую часть памяти используют множество объектов одного класса с частично одинаковым состоянием.
  2. Поделите поля этого класса на две части:

    • внутреннее состояние — значение этих полей одинаковое для большого числа объектов.
    • внешнее состояние (контекст) — значения полей уникальны для каждого объекта.
  3. Оставьте поля внутреннего состояние в классе.

  4. Превратите поля внешнего состояния в аргументы методов, где эти поля использовались. Затем, удалите поля из класса.

  5. Создайте Фабрику, которая будет кешировать и повторно отдавать уже созданные объекты.

  6. Клиент должен запрашивать легковеса с определённым внутренним состоянием из этой фабрики, а не создавать его напрямую.

  7. Клиент должен сам вычислять значения внешнего состояния (контекст) и передавать его в методы объекта легковеса.

Преимущества и недостатки

  • Экономит оперативную память.
  • Расходует процессорное время на поиск и вычисление контекста.
  • Усложняет код программы за счёт множества дополнительных классов.

Отношения с другими паттернами

  • Компоновщик часто совмещают с Легковесом, чтобы реализовать общие ветки дерева и сэкономить при этом память.

  • Легковес показывает как создавать много мелких объектов, а Фасад показывает как создать один объект, который отображает целую подсистему.

Реализация в различных языках программирования

Java