Весняний РОЗПРОДАЖ

Компонувальник

Також відомий як: Дерево, Composite

Суть патерна

Компонувальник — це структурний патерн проектування, що дає змогу згрупувати декілька об’єктів у деревоподібну структуру, а потім працювати з нею так, ніби це одиничний об’єкт.

Патерн Компонувальник

Проблема

Патерн Компонувальник має сенс тільки в тих випадках, коли основна модель вашої програми може бути структурована у вигляді дерева.

Наприклад, є два об’єкти — Продукт і Коробка. Коробка може містити кілька Продуктів та інших Коробок меншого розміру. Останні, в свою чергу, також містять або Продукти, або Коробки і так далі.

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

Структура складного замовлення

Замовлення може складатися з різних продуктів, запакованих у власні коробки.

Якщо спробувати вирішити завдання напролом, тоді потрібно відкрити усі коробки замовлення, перебрати продукти й порахувати їхню загальну вартість. Але це занадто велика морока, оскільки типи коробок і їхній вміст можуть бути вам невідомі заздалегідь. Крім того, наперед невідомою є і кількість рівнів вкладеності коробок, тому перебрати коробки простим циклом не вийде.

Рішення

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

Продукт просто поверне свою вартість, а Коробка запитає про вартість кожного предмета всередині себе і поверне суму результатів. Якщо одним із внутрішніх предметів виявиться трохи менша коробка, вона теж буде перебирати власний вміст, і так далі, допоки не порахується вміст усіх складових частин.

Рішення з Компонувальником

Компонувальник рекурсивно запускає дію по всіх компонентах дерева — від коріння до листя.

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

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

Приклад армійської структури

Приклад армійської структури.

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

Структура

Структура класів патерна КомпонувальникСтруктура класів патерна Компонувальник
  1. Компонент описує загальний інтерфейс для простих і складових компонентів дерева.

  2. Лист — це простий компонент дерева, який не має відгалужень. Класи листя міститимуть більшу частину корисного коду, тому що їм нікому передавати його виконання.

  3. Контейнер (або композит) — це складовий компонент дерева. Він містить набір дочірніх компонентів, але нічого не знає про їхні типи. Це можуть бути як прості компоненти-листя, так і інші компоненти-контейнери. Проте, це не проблема, якщо усі дочірні компоненти дотримуються єдиного інтерфейсу.

    Методи контейнера переадресовують основну роботу своїм дочірнім компонентам, хоча можуть додавати щось своє до результату.

  4. Клієнт працює з деревом через загальний інтерфейс компонентів.

    Завдяки цьому, клієнту не важливо, що перед ним знаходиться — простий чи складовий компонент дерева.

Псевдокод

У цьому прикладі Компонувальник допомагає реалізувати вкладені геометричні фігури.

Структура класів прикладу патерна Компонувальник

Приклад редактора геометричних фігур.

Клас CompoundGraphic може містити будь-яку кількість підфігур, включно з такими самими контейнерами, як і він сам. Контейнер реалізує ті ж самі методи, що і прості фігури. Але замість безпосередньої дії він передає виклики всім вкладеним компонентам, використовуючи рекурсію. Потім він як би «підсумовує» результати всіх вкладених фігур.

Клієнтський код працює з усіма фігурами через загальний інтерфейс фігур і не знає що перед ним — проста фігура чи складова. Це дозволяє клієнтському коду працювати з деревами об’єктів будь-якої складності, не прив’язуючись до конкретних класів об’єктів, що формують дерево.

// Загальний інтерфейс компонентів.
interface Graphic is
    method move(x, y)
    method draw()

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

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

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

    method draw() is
        // Намалювати крапку у координатах X, Y.

// Компоненти можуть розширювати інші компоненти.
class Circle extends Dot is
    field radius

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

    method draw() is
        // Намалювати коло в координатах X, Y з радіусом R.

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

    method add(child: Graphic) is
        // Додати компонент до списка дочірніх.

    method remove(child: Graphic) is
        // Прибрати компонент зі списку дочірніх.

    method move(x, y) is
        foreach (child in children) do
            child.move(x, y)

    method draw() is
        // 1. Для кожного дочірнього компонента:
        //     - Відобразити компонент.
        //     - Визначити координати максимальної межі.
        // 2. Намалювати пунктирну межу навколо всієї області.


// Програма працює одноманітно, як з одиничними компонентами,
// так і з цілими групами компонентів.
class ImageEditor is
    field all: CompoundGraphic

    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()
        foreach (component in components) do
            group.add(component)
            all.remove(component)
        all.add(group)
        // Усі компоненти будуть промальованими.
        all.draw()

Застосування

Якщо вам потрібно представити деревоподібну структуру об’єктів.

Патерн Компонувальник пропонує зберігати в складових об’єктах посилання на інші прості або складові об’єкти. Вони, у свою чергу, теж можуть зберігати свої вкладені об’єкти і так далі. У підсумку, ви можете будувати складну деревоподібну структуру даних, використовуючи всього два основних різновида об’єктів.

Якщо клієнти повинні однаково трактувати прості та складові об’єкти.

Завдяки тому, що прості та складові об’єкти реалізують спільний інтерфейс, клієнту байдуже, з яким саме об’єктом він працюватиме.

Кроки реалізації

  1. Переконайтеся, що вашу бізнес-логіку можна представити як деревоподібну структуру. Спробуйте розбити її на прості компоненти й контейнери. Пам’ятайте, що контейнери можуть містити як прості компоненти, так і інші вкладені контейнери.

  2. Створіть загальний інтерфейс компонентів, який об’єднає операції контейнерів та простих компонентів дерева. Інтерфейс буде вдалим, якщо ви зможете використовувати його, щоб взаємозаміняти прості й складові компоненти без втрати сенсу.

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

  4. Створіть клас компонентів-контейнерів і додайте до нього масив для зберігання посилань на вкладені компоненти. Цей масив повинен бути здатен містити як прості, так і складові компоненти, тому переконайтеся, що його оголошено з типом інтерфейсу компонентів.

    Реалізуйте в контейнері методи інтерфейсу компонентів, пам’ятаючи про те, що контейнери повинні делегувати основну роботу своїм дочірнім компонентам.

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

    Майте на увазі, що методи додавання/видалення дочірніх компонентів можна оголосити також і в інтерфейсі компонентів. Так, це порушить принцип розділення інтерфейсу, тому що реалізації методів будуть порожніми в компонентах-листях. Проте усі компоненти дерева стануть дійсно однаковими для клієнта.

Переваги та недоліки

  • Спрощує архітектуру клієнта при роботі зі складним деревом компонентів.
  • Полегшує додавання нових видів компонентів.
  • Створює занадто загальний дизайн класів.

Відносини з іншими патернами

  • Будівельник дозволяє покроково конструювати дерево Компонувальника.

  • Ланцюжок обов’язків часто використовують разом з Компонувальником. У цьому випадку запит передається від дочірніх компонентів до їхніх батьків.

  • Ви можете обходити дерево Компонувальника, використовуючи Ітератор.

  • Ви можете виконати якусь дію над усім деревом Компонувальника за допомогою Відвідувача.

  • Компонувальник часто поєднують з Легковаговиком, щоб реалізувати спільні гілки дерева та заощадити при цьому пам’ять.

  • Компонувальник та Декоратор мають схожі структури класів, бо обидва побудовані на рекурсивній вкладеності. Вона дозволяє зв’язати в одну структуру нескінченну кількість об’єктів.

    Декоратор обгортає тільки один об’єкт, а вузол Компонувальника може мати багато дітей. Декоратор додає вкладеному об’єкту нової функціональності, а Компонувальник не додає нічого нового, але «підсумовує» результати всіх своїх дітей.

    Але вони можуть і співпрацювати: Компонувальник може використовувати Декоратор, щоб перевизначити функції окремих частин дерева компонентів.

  • Архітектура, побудована на Компонувальниках та Декораторах, часто може поліпшуватися за рахунок впровадження Прототипу. Він дозволяє клонувати складні структури об’єктів, а не збирати їх заново.

Приклади реалізації патерна

Компонувальник на C# Компонувальник на C++ Компонувальник на Go Компонувальник на Java Компонувальник на PHP Компонувальник на Python Компонувальник на Ruby Компонувальник на Rust Компонувальник на Swift Компонувальник на TypeScript