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 — это идеальный паттерн для работы с древовидными структурами!