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

Composite

Паттерн Composite

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

Проблема

Представьте, что вы создаете приложение для работы с файловой системой. У вас есть два типа объектов: файлы и папки. Папки могут содержать файлы и другие папки внутри себя.

Как бы вы реализовали функцию подсчета размера папки? Папка должна была бы знать о всех своих "детях", будь то файлы или другие папки, и запрашивать у них размер, а затем суммировать результаты.

Это может быстро стать сложным, поскольку вам нужно знать различие между файлами и папками, обрабатывать папки по-разному и т.д. При добавлении новых типов элементов файловой системы код станет еще более запутанным.

Решение

Паттерн Composite предлагает работать с файлами и папками через единый интерфейс. Он объявляет общий интерфейс для простых и составных объектов файловой системы.


Структура

Component
├── operation(): string
├── add(component: Component): void
├── remove(component: Component): void
└── getChild(index: number): Component

Leaf
└── operation(): string

Composite
├── children: Component[]
├── operation(): string
├── add(component: Component): void
├── remove(component: Component): void
└── getChild(index: number): Component

Пример реализации

JavaScript

// Базовый компонент
class FileSystemComponent {
    constructor(name) {
        this.name = name;
    }
    
    getName() {
        return this.name;
    }
    
    getSize() {
        throw new Error('Метод getSize должен быть переопределен');
    }
    
    display(indent = 0) {
        throw new Error('Метод display должен быть переопределен');
    }
    
    // Методы для работы с детьми (по умолчанию не поддерживаются)
    add(component) {
        throw new Error('Добавление компонентов не поддерживается');
    }
    
    remove(component) {
        throw new Error('Удаление компонентов не поддерживается');
    }
    
    getChild(index) {
        throw new Error('Получение дочерних компонентов не поддерживается');
    }
}

// Листовой компонент (файл)
class File extends FileSystemComponent {
    constructor(name, size) {
        super(name);
        this.size = size;
    }
    
    getSize() {
        return this.size;
    }
    
    display(indent = 0) {
        const spaces = '  '.repeat(indent);
        console.log(`${spaces}File ${this.name} (${this.size} bytes)`);
    }
}

// Составной компонент (папка)
class Folder extends FileSystemComponent {
    constructor(name) {
        super(name);
        this.children = [];
    }
    
    getSize() {
        let totalSize = 0;
        for (const child of this.children) {
            totalSize += child.getSize();
        }
        return totalSize;
    }
    
    display(indent = 0) {
        const spaces = '  '.repeat(indent);
        console.log(`${spaces}Folder ${this.name} (${this.getSize()} bytes)`);
        
        for (const child of this.children) {
            child.display(indent + 1);
        }
    }
    
    add(component) {
        this.children.push(component);
    }
    
    remove(component) {
        const index = this.children.indexOf(component);
        if (index > -1) {
            this.children.splice(index, 1);
        }
    }
    
    getChild(index) {
        return this.children[index];
    }
    
    getChildren() {
        return this.children;
    }
}

// Использование
function createFileSystem() {
    // Создаем файлы
    const file1 = new File('document.txt', 1024);
    const file2 = new File('image.jpg', 2048);
    const file3 = new File('script.js', 512);
    const file4 = new File('style.css', 256);
    
    // Создаем папки
    const documents = new Folder('Documents');
    const images = new Folder('Images');
    const web = new Folder('Web');
    const root = new Folder('Root');
    
    // Строим структуру
    documents.add(file1);
    images.add(file2);
    web.add(file3);
    web.add(file4);
    
    root.add(documents);
    root.add(images);
    root.add(web);
    
    return root;
}

// Тестируем
const fileSystem = createFileSystem();

console.log('=== Структура файловой системы ===');
fileSystem.display();

console.log('\n=== Размеры ===');
console.log(`Общий размер: ${fileSystem.getSize()} bytes`);
console.log(`Размер папки Documents: ${fileSystem.getChild(0).getSize()} bytes`);
console.log(`Размер папки Web: ${fileSystem.getChild(2).getSize()} bytes`);

TypeScript с интерфейсами

// Интерфейс компонента
interface Graphic {
    move(x: number, y: number): void;
    draw(): void;
    getBounds(): { x: number; y: number; width: number; height: number };
}

// Листовой компонент
class Dot implements Graphic {
    private x: number;
    private y: number;
    
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    
    move(x: number, y: number): void {
        this.x += x;
        this.y += y;
    }
    
    draw(): void {
        console.log(`Рисуем точку в позиции (${this.x}, ${this.y})`);
    }
    
    getBounds(): { x: number; y: number; width: number; height: number } {
        return { x: this.x, y: this.y, width: 1, height: 1 };
    }
}

class Circle implements Graphic {
    private x: number;
    private y: number;
    private radius: number;
    
    constructor(x: number, y: number, radius: number) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    
    move(x: number, y: number): void {
        this.x += x;
        this.y += y;
    }
    
    draw(): void {
        console.log(`Рисуем круг в позиции (${this.x}, ${this.y}) с радиусом ${this.radius}`);
    }
    
    getBounds(): { x: number; y: number; width: number; height: number } {
        return {
            x: this.x - this.radius,
            y: this.y - this.radius,
            width: this.radius * 2,
            height: this.radius * 2
        };
    }
}

// Составной компонент
class CompoundGraphic implements Graphic {
    private children: Graphic[] = [];
    
    add(child: Graphic): void {
        this.children.push(child);
    }
    
    remove(child: Graphic): void {
        const index = this.children.indexOf(child);
        if (index > -1) {
            this.children.splice(index, 1);
        }
    }
    
    move(x: number, y: number): void {
        for (const child of this.children) {
            child.move(x, y);
        }
    }
    
    draw(): void {
        console.log('Рисуем составную графику:');
        for (const child of this.children) {
            child.draw();
        }
    }
    
    getBounds(): { x: number; y: number; width: number; height: number } {
        if (this.children.length === 0) {
            return { x: 0, y: 0, width: 0, height: 0 };
        }
        
        let minX = Infinity, minY = Infinity;
        let maxX = -Infinity, maxY = -Infinity;
        
        for (const child of this.children) {
            const bounds = child.getBounds();
            minX = Math.min(minX, bounds.x);
            minY = Math.min(minY, bounds.y);
            maxX = Math.max(maxX, bounds.x + bounds.width);
            maxY = Math.max(maxY, bounds.y + bounds.height);
        }
        
        return {
            x: minX,
            y: minY,
            width: maxX - minX,
            height: maxY - minY
        };
    }
}

// Использование
const dot1 = new Dot(1, 2);
const dot2 = new Dot(3, 4);
const circle = new Circle(5, 6, 3);

const compound = new CompoundGraphic();
compound.add(dot1);
compound.add(dot2);
compound.add(circle);

console.log('=== Рисование составной графики ===');
compound.draw();

console.log('\n=== Перемещение ===');
compound.move(10, 10);
compound.draw();

console.log('\n=== Границы ===');
const bounds = compound.getBounds();
console.log(`Границы: x=${bounds.x}, y=${bounds.y}, width=${bounds.width}, height=${bounds.height}`);

Когда использовать

Используйте Composite, когда:

  • Нужно представить иерархии объектов "часть-целое"
  • Клиенты должны единообразно трактовать составные и индивидуальные объекты
  • Структура может иметь любую сложность и динамически изменяться
  • Нужно работать с древовидными структурами

Преимущества

  • Работа с составными структурами как с единичными объектами
  • Упрощает клиентский код
  • Легко добавлять новые типы компонентов
  • Принцип открытости/закрытости: можно добавлять новые элементы без изменения существующего кода

Недостатки

  • Может сделать дизайн слишком общим
  • Сложно ограничить типы компонентов в композите
  • Может нарушить принцип единственной ответственности

Отличия от других паттернов

  • Decorator — добавляет функциональность объекту
  • Facade — предоставляет упрощенный интерфейс к подсистеме
  • Flyweight — экономит память, разделяя общее состояние

Реальный пример

// Composite для UI компонентов
class UIComponent {
    constructor(name) {
        this.name = name;
        this.children = [];
    }
    
    render() {
        throw new Error('Метод render должен быть переопределен');
    }
    
    add(component) {
        this.children.push(component);
    }
    
    remove(component) {
        const index = this.children.indexOf(component);
        if (index > -1) {
            this.children.splice(index, 1);
        }
    }
}

class Container extends UIComponent {
    render() {
        let html = `<div class="container" id="${this.name}">\n`;
        for (const child of this.children) {
            html += `  ${child.render()}\n`;
        }
        html += '</div>';
        return html;
    }
}

class Button extends UIComponent {
    constructor(name, text) {
        super(name);
        this.text = text;
    }
    
    render() {
        return `<button id="${this.name}">${this.text}</button>`;
    }
}

class Input extends UIComponent {
    constructor(name, placeholder) {
        super(name);
        this.placeholder = placeholder;
    }
    
    render() {
        return `<input id="${this.name}" placeholder="${this.placeholder}">`;
    }
}

// Использование
const form = new Container('login-form');
const usernameInput = new Input('username', 'Имя пользователя');
const passwordInput = new Input('password', 'Пароль');
const submitButton = new Button('submit', 'Войти');

form.add(usernameInput);
form.add(passwordInput);
form.add(submitButton);

console.log(form.render());

Composite — это идеальный паттерн для работы с древовидными структурами!

Copyright © 2026