Flyweight Pattern — Приспособленец

Экономия памяти за счёт разделения общего состояния между множеством объектов. В JS реализуется естественно через прототипы.

Проблема

Нужно тысячи однотипных объектов: враги в игре, частицы, иконки, символы текста, маркеры на карте. 90% полей у них одинаковые. Хранить каждый отдельно — миллионы байт в памяти.

Где используется

  • Текстовые редакторы: символы шрифта как Flyweight
  • Частицы в играх: тысячи пуль/звёздочек с общей геометрией и текстурой
  • Пины на карте: маркеры с общим SVG-иконкой, уникальные координаты
  • DOM-события через делегирование: один обработчик для тысяч элементов
  • Кэш объектов: Symbol.for — глобальный Flyweight для символов

Решение

Два состояния:

  • Внутреннее (intrinsic) — общее, разделяемое. Хранится в эталоне.
  • Внешнее (extrinsic) — уникальное. Хранится в инстансе.

В JS можно через прототипы: один класс, эталонный объект как прототип.

Реализации

Классический GoF (Tree Factory)

class TreeType {
  constructor(name, color, texture) {
    this.name = name;
    this.color = color;
    this.texture = texture; // большой объект текстуры
  }
  draw(canvas, x, y, scale) { canvas.drawImage(this.texture, x, y, scale, scale); }
}

class TreeTypeFactory {
  static #cache = new Map();
  static getTreeType(name, color, texture) {
    const key = `${name}_${color}`;
    if (!this.#cache.has(key)) this.#cache.set(key, new TreeType(name, color, texture));
    return this.#cache.get(key);
  }
  static getCount { return this.#cache.size; }
}

class Tree {
  constructor(x, y, scale, type) {
    this.x = x; this.y = y; this.scale = scale;
    this.type = type; // ссылка на разделяемый TreeType
  }
  draw(canvas) { this.type.draw(canvas, this.x, this.y, this.scale); }
}

// 10 000 деревьев, но только 3 TreeType в памяти
const trees = ;
for (let i = 0; i < 10000; i++) {
  const types = ['дуб', 'берёза', 'ель'];
  const typeName = types[i % 3];
  const type = TreeTypeFactory.getTreeType(typeName, '#2d5a27', textures[typeName]);
  trees.push(new Tree(Math.random * 800, Math.random * 600, 1, type));
}
TreeTypeFactory.getCount; // 3

JS-вариант через прототипы (один класс)

const cache = new Map();

class Interval {
  constructor(msec, callback) {
    this.callback = callback;            // уникальное — в инстансе
    let timer = cache.get(msec);
    if (!timer) {
      timer = makeTimer(msec);           // эталон создаётся один раз
      cache.set(msec, timer);
    }
    Object.setPrototypeOf(this, timer);  // общее — через прототип
  }
  remove {
    const timer = Object.getPrototypeOf(this);
    timer.listeners.delete(this.callback);
  }
}
// Тысячи Interval(1000, ...) делят один timer

Через Map (функциональный)

const markerPool = new Map();
function getMarkerIcon(type) {
  if (!markerPool.has(type)) markerPool.set(type, createSVGIcon(type));
  return markerPool.get(type);
}

const markers = locations.map(loc => ({
  lat: loc.lat,
  lng: loc.lng,
  icon: getMarkerIcon(loc.type)
}));

DOM-делегирование как Flyweight

// Один обработчик для тысяч элементов — extrinsic state в data-атрибутах
list.addEventListener('click', (e) => {
  const item = e.target.closest('.item-button');
  if (!item) return;
  handleItemClick(item.dataset.id);
});

Где используется в JS-экосистеме

  • V8 hidden classes — внутренний flyweight для объектов одной формы
  • Геймдев: тысячи врагов с общим прототипом (HP, damage, animations)
  • Symbol.for(str) — глобальный реестр символов = flyweight
  • String interning — браузеры дедуплицируют строки
  • React reconciliation: ключи и общие props между элементами

Подводные камни

  • Flyweight vs Prototype: Prototype клонирует целиком; Flyweight шарит общее и хранит уникальное отдельно.
  • Flyweight vs Singleton: Singleton — один объект на процесс; Flyweight — много объектов с общим состоянием.
  • Мутации эталона ломают всех — общее состояние должно быть read-only.
  • В JS легко напортачить с прототипами: setPrototypeOf медленный, в hot path лучше избегать.
  • Преждевременная оптимизация: Flyweight усложняет код. Применяй только если профилирование показало проблему с памятью.
  • Путаница с Proxy и Boxing/Container — общее: за абстракцией стоит другая.

Главные тезисы автора

  • «Нативный для JS способ — через прототипное наследование с одним классом».
  • «Часть свойств в лёгком классе, часть — в общем».
  • «Прототипные цепочки JS ближе всего к Flyweight» (не к Prototype-паттерну).
  • «Это реально JS way: паттерн на одном классе вместо трёх».
  • Критерий паттерна — переиспользуемость: процедурный однострочник для таймеров — это не Flyweight.

🎓 Источники

См. также