Компоновщик (Composite)

Компоновщик (Composite)

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

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

Паттерн Компоновщик

Проблема

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

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

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

Структура сложного заказа
Заказ может состоять из различных продуктов, упакованных в собственные коробки.

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

Решение

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

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

Решение с Компоновщиком
Компоновщик рекурсивно запускает действие по всем элементам дерева — от корня к листьям.

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

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

Пример армейской структуры
Пример армейской структуры.

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

Структура

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

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

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

  3. Контейнер (или композит) — это составной компонент дерева. Он содержит набор дочерних компонентов, но ничего не знает об их типах. Это могут быть как простые компоненты-листья, так и другие компоненты-контейнеры. Но это не является проблемой, если все дочерние компоненты следуют единому интерфейсу.

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

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

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

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

Когда вам нужно представить древовидную структуру объектов.

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

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

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

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

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

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

  3. Создайте класс компонентов-листьев, не имеющих дальнейших ответвлений. Имейте в виду, что программа может содержать несколько таких классов.

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

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

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

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

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

  • Упрощает архитектуру клиента при работе со сложным деревом компонентов.

  • Облегчает добавление новых видов компонентов.

  • Создаёт слишком общий дизайн классов.

Пример

# 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