Компонувальник
Суть патерна
Компонувальник — це структурний патерн проектування, що дає змогу згрупувати декілька об’єктів у деревоподібну структуру, а потім працювати з нею так, ніби це одиничний об’єкт.
Проблема
Патерн Компонувальник має сенс тільки в тих випадках, коли основна модель вашої програми може бути структурована у вигляді дерева.
Наприклад, є два об’єкти — Продукт
і Коробка
. Коробка
може містити кілька Продуктів
та інших Коробок
меншого розміру. Останні, в свою чергу, також містять або Продукти
, або Коробки
і так далі.
Тепер, припустімо, що ваші Продукти
й Коробки
можуть бути частиною замовлень. При цьому замовлення може містити як звичайні Продукт
без пакування, так і наповнені змістом Коробки
. Ваше завдання полягає в тому, щоб дізнатися вартість всього замовлення.
Якщо спробувати вирішити завдання напролом, тоді потрібно відкрити усі коробки замовлення, перебрати продукти й порахувати їхню загальну вартість. Але це занадто велика морока, оскільки типи коробок і їхній вміст можуть бути вам невідомі заздалегідь. Крім того, наперед невідомою є і кількість рівнів вкладеності коробок, тому перебрати коробки простим циклом не вийде.
Рішення
Компонувальник пропонує розглядати Продукт
і Коробку
через єдиний інтерфейс зі спільним методом отримання ціни.
Продукт
просто поверне свою вартість, а Коробка
запитає про вартість кожного предмета всередині себе і поверне суму результатів. Якщо одним із внутрішніх предметів виявиться трохи менша коробка, вона теж буде перебирати власний вміст, і так далі, допоки не порахується вміст усіх складових частин.
Для вас як клієнта важливим є те, що вже не потрібно нічого знати про структуру замовлень. Ви викликаєте метод отримання ціни, він повертає цифру, і ви не «тонете» в горах картону та скотчу.
Аналогія з життя
Армії більшості країн можуть бути представлені у вигляді перевернутих дерев. На нижньому рівні у вас солдати, далі взводи, далі полки, а далі цілі армії. Накази віддаються зверху вниз структурою командування до тих пір, поки вони не доходять до конкретного солдата.
Структура
-
Компонент описує загальний інтерфейс для простих і складових компонентів дерева.
-
Лист — це простий компонент дерева, який не має відгалужень. Класи листя міститимуть більшу частину корисного коду, тому що їм нікому передавати його виконання.
-
Контейнер (або композит) — це складовий компонент дерева. Він містить набір дочірніх компонентів, але нічого не знає про їхні типи. Це можуть бути як прості компоненти-листя, так і інші компоненти-контейнери. Проте, це не проблема, якщо усі дочірні компоненти дотримуються єдиного інтерфейсу.
Методи контейнера переадресовують основну роботу своїм дочірнім компонентам, хоча можуть додавати щось своє до результату.
-
Клієнт працює з деревом через загальний інтерфейс компонентів.
Завдяки цьому, клієнту не важливо, що перед ним знаходиться — простий чи складовий компонент дерева.
Псевдокод
У цьому прикладі Компонувальник допомагає реалізувати вкладені геометричні фігури.
Клас CompoundGraphic
може містити будь-яку кількість підфігур, включно з такими самими контейнерами, як і він сам. Контейнер реалізує ті ж самі методи, що і прості фігури. Але замість безпосередньої дії він передає виклики всім вкладеним компонентам, використовуючи рекурсію. Потім він як би «підсумовує» результати всіх вкладених фігур.
Клієнтський код працює з усіма фігурами через загальний інтерфейс фігур і не знає що перед ним — проста фігура чи складова. Це дозволяє клієнтському коду працювати з деревами об’єктів будь-якої складності, не прив’язуючись до конкретних класів об’єктів, що формують дерево.
Застосування
Якщо вам потрібно представити деревоподібну структуру об’єктів.
Патерн Компонувальник пропонує зберігати в складових об’єктах посилання на інші прості або складові об’єкти. Вони, у свою чергу, теж можуть зберігати свої вкладені об’єкти і так далі. У підсумку, ви можете будувати складну деревоподібну структуру даних, використовуючи всього два основних різновида об’єктів.
Якщо клієнти повинні однаково трактувати прості та складові об’єкти.
Завдяки тому, що прості та складові об’єкти реалізують спільний інтерфейс, клієнту байдуже, з яким саме об’єктом він працюватиме.
Кроки реалізації
-
Переконайтеся, що вашу бізнес-логіку можна представити як деревоподібну структуру. Спробуйте розбити її на прості компоненти й контейнери. Пам’ятайте, що контейнери можуть містити як прості компоненти, так і інші вкладені контейнери.
-
Створіть загальний інтерфейс компонентів, який об’єднає операції контейнерів та простих компонентів дерева. Інтерфейс буде вдалим, якщо ви зможете використовувати його, щоб взаємозаміняти прості й складові компоненти без втрати сенсу.
-
Створіть клас компонентів-листя, які не мають подальших відгалужень. Майте на увазі, що програма може містити декілька таких класів.
-
Створіть клас компонентів-контейнерів і додайте до нього масив для зберігання посилань на вкладені компоненти. Цей масив повинен бути здатен містити як прості, так і складові компоненти, тому переконайтеся, що його оголошено з типом інтерфейсу компонентів.
Реалізуйте в контейнері методи інтерфейсу компонентів, пам’ятаючи про те, що контейнери повинні делегувати основну роботу своїм дочірнім компонентам.
-
Додайте операції додавання й видалення дочірніх компонентів до класу контейнерів.
Майте на увазі, що методи додавання/видалення дочірніх компонентів можна оголосити також і в інтерфейсі компонентів. Так, це порушить принцип розділення інтерфейсу, тому що реалізації методів будуть порожніми в компонентах-листях. Проте усі компоненти дерева стануть дійсно однаковими для клієнта.
Переваги та недоліки
- Спрощує архітектуру клієнта при роботі зі складним деревом компонентів.
- Полегшує додавання нових видів компонентів.
- Створює занадто загальний дизайн класів.
Відносини з іншими патернами
-
Будівельник дозволяє покроково конструювати дерево Компонувальника.
-
Ланцюжок обов’язків часто використовують разом з Компонувальником. У цьому випадку запит передається від дочірніх компонентів до їхніх батьків.
-
Ви можете обходити дерево Компонувальника, використовуючи Ітератор.
-
Ви можете виконати якусь дію над усім деревом Компонувальника за допомогою Відвідувача.
-
Компонувальник часто поєднують з Легковаговиком, щоб реалізувати спільні гілки дерева та заощадити при цьому пам’ять.
-
Компонувальник та Декоратор мають схожі структури класів, бо обидва побудовані на рекурсивній вкладеності. Вона дозволяє зв’язати в одну структуру нескінченну кількість об’єктів.
Декоратор обгортає тільки один об’єкт, а вузол Компонувальника може мати багато дітей. Декоратор додає вкладеному об’єкту нової функціональності, а Компонувальник не додає нічого нового, але «підсумовує» результати всіх своїх дітей.
Але вони можуть і співпрацювати: Компонувальник може використовувати Декоратор, щоб перевизначити функції окремих частин дерева компонентів.
-
Архітектура, побудована на Компонувальниках та Декораторах, часто може поліпшуватися за рахунок впровадження Прототипу. Він дозволяє клонувати складні структури об’єктів, а не збирати їх заново.