Flyweight
Flyweight — это структурный паттерн проектирования, который позволяет вместить больше объектов в доступную оперативную память за счет общего переиспользования одинаковых частей состояния между множеством объектов.
Проблема
Представьте, что вы создаете игру, в которой игроки могут перемещаться по карте и стрелять друг в друга. Когда игрок нажимает на спусковой крючок, игра создает пулю и отправляет её в полет по карте. Каждая пуля представлена отдельным объектом, содержащим такие данные, как координаты, вектор движения, скорость, цвет, спрайт и так далее.
Вскоре вы понимаете, что ваша игра не может обрабатывать более 100 пуль одновременно, потому что доступная оперативная память заканчивается.
Решение
Паттерн Flyweight предлагает прекратить хранить внешнее состояние в объекте, а вместо этого передавать его в специальные методы. В результате одинаковые объекты можно переиспользовать в различных контекстах.
Структура
Flyweight
└── operation(extrinsicState): void
ConcreteFlyweight
├── intrinsicState: string
└── operation(extrinsicState): void
UnsharedConcreteFlyweight
├── allState: string
└── operation(extrinsicState): void
FlyweightFactory
├── flyweights: Map<string, Flyweight>
├── getFlyweight(key: string): Flyweight
└── getFlyweightsCount(): number
Client
└── operation(): void
Пример реализации
JavaScript
// Внутреннее состояние (разделяемое)
class TreeType {
constructor(name, color, texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
render(canvas, x, y) {
console.log(`Отрисовка ${this.name} дерева (${this.color}, ${this.texture}) в позиции (${x}, ${y})`);
}
}
// Фабрика для создания и управления типами деревьев
class TreeTypeFactory {
constructor() {
this.treeTypes = new Map();
}
getTreeType(name, color, texture) {
const key = `${name}_${color}_${texture}`;
if (!this.treeTypes.has(key)) {
console.log(`Создание нового типа дерева: ${key}`);
this.treeTypes.set(key, new TreeType(name, color, texture));
} else {
console.log(`Переиспользование существующего типа дерева: ${key}`);
}
return this.treeTypes.get(key);
}
getTreeTypesCount() {
return this.treeTypes.size;
}
}
// Внешнее состояние (неразделяемое)
class Tree {
constructor(x, y, treeType) {
this.x = x;
this.y = y;
this.treeType = treeType;
}
render(canvas) {
this.treeType.render(canvas, this.x, this.y);
}
}
// Лес (контекст)
class Forest {
constructor() {
this.trees = [];
this.treeTypeFactory = new TreeTypeFactory();
}
plantTree(x, y, name, color, texture) {
const treeType = this.treeTypeFactory.getTreeType(name, color, texture);
const tree = new Tree(x, y, treeType);
this.trees.push(tree);
}
render(canvas) {
console.log(`=== Отрисовка леса с ${this.trees.length} деревьями ===`);
for (const tree of this.trees) {
tree.render(canvas);
}
}
getTreeTypesCount() {
return this.treeTypeFactory.getTreeTypesCount();
}
}
// Использование
const forest = new Forest();
// Сажаем деревья (многие с одинаковыми характеристиками)
forest.plantTree(1, 1, 'Дуб', 'Коричневый', 'Шероховатый');
forest.plantTree(2, 1, 'Дуб', 'Коричневый', 'Шероховатый');
forest.plantTree(3, 1, 'Дуб', 'Коричневый', 'Шероховатый');
forest.plantTree(1, 2, 'Сосна', 'Зеленый', 'Гладкий');
forest.plantTree(2, 2, 'Сосна', 'Зеленый', 'Гладкий');
forest.plantTree(3, 2, 'Береза', 'Белый', 'Гладкий');
forest.plantTree(1, 3, 'Дуб', 'Коричневый', 'Шероховатый');
forest.plantTree(2, 3, 'Сосна', 'Зеленый', 'Гладкий');
console.log(`Всего деревьев: ${forest.trees.length}`);
console.log(`Уникальных типов деревьев: ${forest.getTreeTypesCount()}`);
forest.render('canvas');
TypeScript с интерфейсами
// Интерфейс приспособленца
interface Character {
display(fontSize: number, color: string): void;
}
// Конкретный приспособленец
class CharacterFlyweight implements Character {
private char: string;
constructor(char: string) {
this.char = char;
}
display(fontSize: number, color: string): void {
console.log(`Отображение символа '${this.char}' размером ${fontSize}px цветом ${color}`);
}
}
// Фабрика приспособленцев
class CharacterFactory {
private characters: Map<string, CharacterFlyweight> = new Map();
getCharacter(char: string): CharacterFlyweight {
if (!this.characters.has(char)) {
console.log(`Создание нового символа: '${char}'`);
this.characters.set(char, new CharacterFlyweight(char));
} else {
console.log(`Переиспользование существующего символа: '${char}'`);
}
return this.characters.get(char)!;
}
getCharactersCount(): number {
return this.characters.size;
}
}
// Контекст (внешнее состояние)
class CharacterContext {
private character: CharacterFlyweight;
private fontSize: number;
private color: string;
constructor(char: string, fontSize: number, color: string, factory: CharacterFactory) {
this.character = factory.getCharacter(char);
this.fontSize = fontSize;
this.color = color;
}
display(): void {
this.character.display(this.fontSize, this.color);
}
}
// Клиент
class TextEditor {
private characters: CharacterContext[] = [];
private factory: CharacterFactory;
constructor() {
this.factory = new CharacterFactory();
}
addCharacter(char: string, fontSize: number, color: string): void {
const context = new CharacterContext(char, fontSize, color, this.factory);
this.characters.push(context);
}
displayText(): void {
console.log('=== Отображение текста ===');
for (const charContext of this.characters) {
charContext.display();
}
}
getUniqueCharactersCount(): number {
return this.factory.getCharactersCount();
}
getTotalCharactersCount(): number {
return this.characters.length;
}
}
// Использование
const editor = new TextEditor();
// Добавляем символы (многие повторяются)
editor.addCharacter('H', 16, 'black');
editor.addCharacter('e', 16, 'black');
editor.addCharacter('l', 16, 'black');
editor.addCharacter('l', 16, 'black'); // Повтор
editor.addCharacter('o', 16, 'black');
editor.addCharacter(' ', 16, 'black');
editor.addCharacter('W', 20, 'blue');
editor.addCharacter('o', 20, 'blue');
editor.addCharacter('r', 20, 'blue');
editor.addCharacter('l', 20, 'blue'); // Повтор
editor.addCharacter('d', 20, 'blue');
editor.addCharacter('!', 20, 'red');
console.log(`Всего символов: ${editor.getTotalCharactersCount()}`);
console.log(`Уникальных символов: ${editor.getUniqueCharactersCount()}`);
editor.displayText();
Когда использовать
Используйте Flyweight, когда:
- Приложение использует большое количество объектов
- Хранение объектов обходится дорого из-за их большого количества
- Большинство состояний объектов можно сделать внешними
- Нужно экономить память в приложениях с ограниченными ресурсами
Преимущества
- Экономит память за счет переиспользования объектов
- Уменьшает количество создаваемых объектов
- Улучшает производительность при работе с большими наборами данных
Недостатки
- Усложняет код из-за разделения внутреннего и внешнего состояния
- Может снизить производительность из-за постоянного поиска объектов
- Требует тщательного анализа для правильного разделения состояния
Отличия от других паттернов
- Singleton — гарантирует единственный экземпляр класса
- Factory Method — создает объекты через подклассы
- Composite — группирует объекты в древовидные структуры
Реальный пример
// Flyweight для иконок в UI
class IconFlyweight {
constructor(iconName, svgPath) {
this.iconName = iconName;
this.svgPath = svgPath;
}
render(size, color) {
console.log(`Отрисовка иконки '${this.iconName}' размером ${size}px цветом ${color}`);
return `<svg width="${size}" height="${size}" fill="${color}">${this.svgPath}</svg>`;
}
}
class IconFactory {
constructor() {
this.icons = new Map();
}
getIcon(iconName) {
if (!this.icons.has(iconName)) {
const svgPath = this.getSvgPath(iconName);
this.icons.set(iconName, new IconFlyweight(iconName, svgPath));
}
return this.icons.get(iconName);
}
getSvgPath(iconName) {
const paths = {
'home': '<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>',
'user': '<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>',
'settings': '<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>'
};
return paths[iconName] || '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>';
}
}
class Button {
constructor(text, iconName, size, color, factory) {
this.text = text;
this.icon = factory.getIcon(iconName);
this.size = size;
this.color = color;
}
render() {
const iconHtml = this.icon.render(this.size, this.color);
console.log(`Кнопка: ${this.text} с иконкой ${this.icon.iconName}`);
return `<button>${iconHtml} ${this.text}</button>`;
}
}
// Использование
const iconFactory = new IconFactory();
const buttons = [
new Button('Главная', 'home', 16, 'blue', iconFactory),
new Button('Профиль', 'user', 16, 'green', iconFactory),
new Button('Настройки', 'settings', 16, 'red', iconFactory),
new Button('Главная', 'home', 20, 'purple', iconFactory), // Повтор
new Button('Профиль', 'user', 20, 'orange', iconFactory) // Повтор
];
console.log(`Всего кнопок: ${buttons.length}`);
console.log(`Уникальных иконок: ${iconFactory.icons.size}`);
buttons.forEach(button => button.render());
Flyweight — это экономия памяти через переиспользование!