Также известен как Дерево, Composite

Компоновщик

Суть паттерна

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

Проблема

Паттерн Компоновщик имеет смысл только тогда, когда основная модуль вашей программы может быть структурирована в виде дерева.

Например, есть два объекта — Продукт и Коробка. Коробка может содержать несколько Продуктов и других Коробок поменьше. Те, в свою очередь, тоже содержат либо Продукты, либо Коробки и так далее.

Теперь, предположим, ваши Продукты и Коробки могут быть частью заказов.

Ваша задача в том, чтобы узнать цену всего заказа. Причём в заказе может быть как просто Продукт без упаковки, так и пустая или составная Коробка.

Если решать задачу в лоб, то вам потребуется открыть все коробки заказа, перебрать все продукты и посчитать их суммарную цену.

Но это слишком хлопотно.

Решение

Компоновщик предлагает рассматривать Продукт и Коробку через единый интерфейс с общим методом получитьЦену().

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

Для вас, клиента, главное, что теперь не нужно ничего о структуре заказов. Вы вызываете метод получитьЦену(), он возвращает цифру, а вы не тонете в горах картона и скотча.

Аналогия из жизни

Подразделения армии

Армии большинства государств — это дерево компоновщика. На нижнем уровне у вас есть солдаты, затем взводы, затем полки, затем целые армии.

Приказы отдаются сверху и спускаются вниз по структуре командования, пока не доходят до конкретного солдата.

Структура

Схема структуры классов паттерна Компоновщик
  1. Компонент определяет общий интерфейс для простых и составных компонентов дерева.

  2. Лист – это простой элемент дерева, не имеющий ответвлений.

    Из-за того, что им некому больше передавать выполнение, классы Листьев будут содержать большую часть полезного кода.

  3. Контейнер (или Композит) — это составной элемент дерева. Содержит дочерние элементы — Листья или другие Контейнеры — но не знает какие именно, так как работает с ними только через общий интерфейс Компонента.

    Методы этого класса переадресуют основную работу своим дочерним компонентам, хотя и могут добавлять что-то своё к результату.

  4. Клиент работает с деревом через интерфейс Компонента.

    Благодаря этому, для клиента нет разницы что перед ним находится — простой или составной компонент дерева.

Псевдокод

В этом примере Компоновщик помогает реализовать вложенные геометрические фигуры. Класс CompoundGraphic может содержать любое количество подфигур, включая такие же контейнеры, как и он сам. Контейнер реализует те же методы, что и простые фигуры. Но вместо непосредственного действия, контейнер передаёт вызовы всем вложенным элементам, используя рекурсию. Затем он как бы "суммирует" результаты всех вложенных фигур. Клиентский код работает со всеми фигурами через общий интерфейс фигур и не знает что перед ним — простая фигура или составная.

Таким образом, паттерн Компоновщик позволяет клиентскому коду работать с деревьями объектов любой сложности, не привязываясь к конкретным классам объектов, формирующих дерево.

// Общий интерфейс компонентов.
interface Graphic is
    method move(x, y)
    method draw()

// Простой компонент.
class Dot implements Graphic is
    field x, y

    constructor Circle(x, y) { ... }

    method move(x, y) is
        this.x += x, this.y += y

    method draw() is
        Draw a dot at X and Y.

// Компоненты могут расширять другие компоненты.
class Circle extends Dot is
    field radius

    constructor Circle(x, y, radius) { ... }

    method move(x, y) is
        this.x = x, this.y = y

    method draw() is
        Draw a circle at X and Y and radius R.

// Контейнер содержит операции добавления/удаления дочерних компонентов. Все
// стандартные операции интерфейса компонентов он делегирует каждому из
// дочерних компонент.
class CompoundGraphic implements Graphic is
    field children: array of Graphic

    method add(child: Graphic) is
        Add child to children array.

    method remove(child: Graphic) is
        Remove child to children array.

    method move(x, y) is
        For each child: child.move(x, y)

    method draw() is
        Go over all children and calculate bounding rectangle.
        Draw a dotted box using calculated values.
        Draw each child.


// Приложение работает как с единичными компонентами, так и целыми группами.
class ImageEditor is
    method load() is
        all = new CompoundGraphic()
        all.add(new Dot(1, 2))
        all.add(new Circle(5, 3, 10))
        // ...

    method groupSelected(components: array of Graphic) is
        group = new CompoundGraphic()
        group.add(components)
        all.remove(components)
        all.add(group)
        // Все компоненты будут отрисованы.
        all.draw()

Применимость

Нужно представить древовидную структуру объектов — контейнеры и содержимое.

Паттерн Компоновщик предлагает хранить в составных объектах ссылки на другие простые или составные объекты. Те, в свою очередь, тоже могут хранить свои вложенные объекты и так далее. В итоге вы можете строить сложную древовидную структуру данных, используя всего два вида объектов.

Клиенты должны единообразно трактовать простые и составные объекты.

Благодаря тому, что простые и составные объекты реализуют общий интерфейс, клиенту безразлично с каким именно объектом ему предстоит работать.

Шаги реализации

  1. Убедитесь, что вашу бизнес-логику можно представить как древовидную структуру. Попытайтесь разбить её на простые элементы и контейнеры. Помните, что контейнеры могут содержать как простые элементы, так и другие контенеры.

  2. Создайте общий интерфейс Компонентов, который объединит операции контейнеров и простых элементов дерева. Интерфейс будет удачным, если вы сможете взаимозаменять простые и сложные компоненты без потери смысла.

  3. Создайте класс Листьев, представляющий простые элементы. Кстати, программа может содержать несколько видов таких классов.

  4. Создайте классы для простых и сложных компонентов. Они должны реализовать общий интерфейс Компонента.

  5. Класс контейнеров должен содержать другие Компоненты, простые или составные.

  6. Контейнеры могут делегировать работу всем внутренними компонентам, через общий интерфейс Компонентов. Благодаря этому, не будет разницы, какие конкретно компоненты находятся в контейнере.

  7. Добавьте операции добавление и удаления дочерних элементов в класс контейнеров.

    Имейте в виду, что методы добавления/удаления дочерних элементов можно поместить и в интерфейс Компонентов. Да, это нарушит принцип разделения интерфейса, так как реализации методов будут пустыми в компонентах-листьях. Но зато все компоненты дерева станут действительно одинаковыми для клиента.

Преимущества и недостатки

  • Упрощает архитектуру клиента при работе со сложным деревом компонентов.
  • Облегчает добавление новых видов компонентов.
  • Создаёт слишком общий дизайн классов.

Отношения с другими паттернами

  • Строитель позволяет пошагово сооружать дерево Компоновщика.

  • Цепочку обязанностей часто используют вместе с Компоновщиком. В этом случае, запрос передаётся от дочерних компонентов к их родителям.

  • Вы можете обходить дерево Компоновщика, используя Итератор.

  • Вы можете выполнить какое-то действие над всем деревом Компоновщика при помощи Посетителя.

  • Компоновщик часто совмещают с Легковесом, чтобы реализовать общие ветки дерева и сэкономить при этом память.

  • Компоновщик и Декоратор имеют похожие структуры классов из-за того, что оба построены на рекурсивной вложенности. Она позволяет связать в одну структуру бесконечное количество объектов.

    Декоратор оборачивает только один объект, а узел Компоновщика может иметь много детей. Декоратор добавляет вложенному объекту новую функциональность, а Компоновщик не добавляет ничего нового, но «суммирует» результаты всех своих детей.

    Но они могут и сотрудничать: Компоновщик может использовать Декоратор, чтобы переопределить функции отдельных частей дерева компонентов.

  • Архитектура, построенная на Компоновщиках и Декораторах часто может быть улучшена за счёт внедрения Прототипа. Он позволил бы клонировать сложные структуры, а не собирать заново.

Реализация в различных языках программирования

Java