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

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

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

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

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

Компонент определяет общий интерфейс для простых и составных компонентов дерева.
Лист — это простой компонент дерева, не имеющий ответвлений.
Из-за того, что им некому больше передавать выполнение, классы листьев будут содержать большую часть полезного кода.
Контейнер (или композит) — это составной компонент дерева. Он содержит набор дочерних компонентов, но ничего не знает об их типах. Это могут быть как простые компоненты-листья, так и другие компоненты-контейнеры. Но это не является проблемой, если все дочерние компоненты следуют единому интерфейсу.
Методы контейнера переадресуют основную работу своим дочерним компонентам, хотя и могут добавлять что-то своё к результату.
Клиент работает с деревом через общий интерфейс компонентов.
Благодаря этому, клиенту не важно, что перед ним находится — простой или составной компонент дерева.
Применимость
Когда вам нужно представить древовидную структуру объектов.
Паттерн Компоновщик предлагает хранить в составных объектах ссылки на другие простые или составные объекты. Те, в свою очередь, тоже могут хранить свои вложенные объекты и так далее. В итоге вы можете строить сложную древовидную структуру данных, используя всего две основные разновидности объектов.
Когда клиенты должны единообразно трактовать простые и составные объекты.
Благодаря тому, что простые и составные объекты реализуют общий интерфейс, клиенту безразлично, с каким именно объектом ему предстоит работать.
Шаги реализации
Убедитесь, что вашу бизнес-логику можно представить как древовидную структуру. Попытайтесь разбить её на простые компоненты и контейнеры. Помните, что контейнеры могут содержать как простые компоненты, так и другие вложенные контейнеры.
Создайте общий интерфейс компонентов, который объединит операции контейнеров и простых компонентов дерева. Интерфейс будет удачным, если вы сможете использовать его, чтобы взаимозаменять простые и составные компоненты без потери смысла.
Создайте класс компонентов-листьев, не имеющих дальнейших ответвлений. Имейте в виду, что программа может содержать несколько таких классов.
Создайте класс компонентов-контейнеров и добавьте в него массив для хранения ссылок на вложенные компоненты. Этот массив должен быть способен содержать как простые, так и составные компоненты, поэтому убедитесь, что он объявлен с типом интерфейса компонентов.
Реализуйте в контейнере методы интерфейса компонентов, помня о том, что контейнеры должны делегировать основную работу своим дочерним компонентам.
Добавьте операции добавления и удаления дочерних компонентов в класс контейнеров.
Имейте в виду, что методы добавления/удаления дочерних компонентов можно поместить и в интерфейс компонентов. Да, это нарушит принцип разделения интерфейса, так как реализации методов будут пустыми в компонентах-листьях. Но зато все компоненты дерева станут действительно одинаковыми для клиента.
Преимущества и недостатки
Упрощает архитектуру клиента при работе со сложным деревом компонентов.
Облегчает добавление новых видов компонентов.
Создаёт слишком общий дизайн классов.
Пример
# include <iostream>
# include <initializer_list>
# include <memory>
# include <vector>
using namespace std;
class Component;
using PtrComponent = shared_ptr<Component>;
using VectorComponent = vector<PtrComponent>;
class Component
{
public:
using value_type = Component;
using size_type = size_t;
using iterator = VectorComponent::const_iterator;
using const_iterator = VectorComponent::const_iterator;
virtual ~Component() = default;
virtual void operation() = 0;
virtual bool isComposite() const { return false; }
virtual bool add(initializer_list<PtrComponent> comp) { return false; }
virtual bool remove(const iterator& it) { return false; }
virtual iterator begin() const { return iterator(); }
virtual iterator end() const { return iterator(); }
};
class Figure : public Component
{
public:
virtual void operation() override { cout << "Figure method;" << endl; }
};
class Camera : public Component
{
public:
virtual void operation() override { cout << "Camera method;" << endl; }
};
class Composite : public Component
{
private:
VectorComponent vec;
public:
Composite() = default;
Composite(PtrComponent first, ...);
void operation() override;
bool isComposite() const override { return true; }
bool add(initializer_list<PtrComponent> list) override;
bool remove(const iterator& it) override { vec.erase(it); return true; }
iterator begin() const override { return vec.begin(); }
iterator end() const override { return vec.end(); }
};
# pragma region Methods
Composite::Composite(PtrComponent first, ...)
{
for (shared_ptr<Component>* ptr = &first; *ptr; ++ptr)
vec.push_back(*ptr);
}
void Composite::operation()
{
cout << "Composite method:" << endl;
for (auto elem : vec)
elem->operation();
}
bool Composite::add(initializer_list<PtrComponent> list)
{
for(auto elem : list)
vec.push_back(elem);
return true;
}
# pragma endregion
int main()
{
using Default = shared_ptr<Component>;
PtrComponent fig = make_shared<Figure>(), cam = make_shared<Camera>();
auto composite1 = make_shared<Composite>(fig, cam, Default{});
composite1->add({ make_shared<Figure>(), make_shared<Camera>() });
composite1->operation();
cout << endl;
auto it = composite1->begin();
composite1->remove(++it);
composite1->operation();
cout << endl;
auto composite2 = make_shared<Composite>(make_shared<Figure>(), composite1, Default());
composite2->operation();
}
Last updated