Composite Pattern — Композит

Единый интерфейс к дереву. Лист, узел, всё дерево — обрабатываются одинаково.

Проблема

Древовидная структура: смета (товар → подгруппа → группа), файловая система (файл → папка), DOM (узел → элемент → дерево). Хочется работать с любым узлом одинаково: получить «общую сумму», «полное имя», «количество». Composite избавляет от условий типа if (item.isGroup) по всему коду.

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

  • DOM-дерево: элемент и документ обрабатываются аналогично
  • Файловая система: файл и папка с одинаковым API для size, print
  • UI-компоненты: кнопка и группа кнопок
  • Корзина интернет-магазина: товар и бандл с getPrice
  • Разрешения: одиночное право и группа прав
  • AST в парсерах

Решение

  • Один абстрактный интерфейс на всё дерево
  • Лист реализует операцию как базовый случай
  • Узел реализует операцию через рекурсивный обход детей

Реализации

Файловая система

class FileSystemItem {
  constructor(name) { this.name = name; }
  getSize { throw new Error('abstract'); }
  print(indent = '') { throw new Error('abstract'); }
}

class File extends FileSystemItem {
  constructor(name, size) { super(name); this.size = size; }
  getSize { return this.size; }
  print(indent = '') { console.log(`${indent}📄 ${this.name} (${this.size} KB)`); }
}

class Directory extends FileSystemItem {
  constructor(name) { super(name); this.children = ; }
  add(item) { this.children.push(item); return this; }
  remove(item) { this.children = this.children.filter(c => c !== item); return this; }
  getSize { return this.children.reduce((sum, c) => sum + c.getSize, 0); }
  print(indent = '') {
    console.log(`${indent}📁 ${this.name}/`);
    this.children.forEach(c => c.print(indent + '  '));
  }
}

const root = new Directory('root');
const src = new Directory('src');
src.add(new File('index.js', 12)).add(new File('utils.js', 8));
root.add(src).add(new File('README.md', 2));
root.print;
console.log('Размер:', root.getSize, 'KB');

Корзина с бандлами

class Product {
  constructor(name, price) { this.name = name; this.price = price; }
  getPrice { return this.price; }
}

class Bundle {
  constructor(name, discount = 0) {
    this.name = name;
    this.items = ;
    this.discount = discount;
  }
  add(item) { this.items.push(item); return this; }
  getPrice {
    const total = this.items.reduce((s, i) => s + i.getPrice, 0);
    return total * (1 - this.discount);
  }
}

const cart = new Bundle('Корзина');
cart.add(new Product('Ноутбук', 80000));
const workBundle = new Bundle('Рабочий', 0.1);
workBundle.add(new Product('Мышь', 3000)).add(new Product('Клавиатура', 5000));
cart.add(workBundle);
cart.getPrice; // 80000 + (8000 * 0.9) = 87200

Простой Composite (одинаковый интерфейс cost)

class CartItem {
  constructor(name, price, qty = 1) { this.name = name; this.price = price; this.qty = qty; }
  cost { return this.price * this.qty; }
}

class CartGroup {
  constructor(name, items = ) { this.name = name; this.items = items; }
  cost { return this.items.reduce((sum, i) => sum + i.cost, 0); }
}

const order = new CartGroup('order', [
  new CartItem('apple', 10, 5),
  new CartGroup('drinks', [
    new CartItem('water', 20, 2),
    new CartItem('cola', 30, 1),
  ]),
]);
order.cost; // 50 + 40 + 30 = 120

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

  • DOM: node.querySelector, node.children, node.appendChild — один API на любом узле
  • React/Vue компоненты — composite-дерево
  • JSON-структуры — естественный composite
  • Filesystem APIfs.stat, readdirRecursive
  • AST в парсерах — composite классическое применение

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

  • Composite vs Facade: Composite — дерево похожих узлов одного интерфейса; корень дерева можно назвать facade'ом, но это специализированный случай.
  • Composite vs Decorator: Decorator оборачивает один узел; Composite собирает много в дерево.
  • Если в дереве узлы слишком разные по поведению — единый интерфейс ломается.
  • Производительность: рекурсивный обход больших деревьев может быть дорогим — нужен кэш или итеративный обход (stack overflow).
  • Нарушение LSP: Leaf реализует методы add/remove с заглушками. Лучше выносить add/remove только в Container.

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

  • «Общий интерфейс — и доступаемся к листочку, к ноде или ко всему дереву».
  • «Корень дерева можно назвать фасадом» — Composite это специализированный Facade.
  • Пример автора: смета с подгруппами — каждый узел знает свою стоимость и сумму подузлов.
  • DOM — канонический пример Composite, который JS-разработчик использует ежедневно.

🎓 Источники

См. также