Lapa Knowledge Base
Структурные паттерны

Flyweight

Паттерн 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 — это экономия памяти через переиспользование!

Copyright © 2026