Намір
Компонує об'єкти у деревоподібні структури для представлення (part-whole?) ієрархій. Композитор дозволяє клієнтам трактувати індивідуальні об'єкти і композиції об'єктів уніфіковано.
Мотивація
Графічні програми на подобі графічних редакторів і систем розроблення схем дозволяють будувати складні діаграми з простих компонентів. Користувач може згрупувати компоненти для формування більших компонентів, які у свою чергу можуть бути згрупованими для формування ще більших компонентів. Проста реалізація може визначити класи для графічних примітивів на подобі типів Text і Lines плюс інші класи, які поводять себе як контейнери для цих примітивів.
Але з цим підходом існує проблема: код, який використовує ці класи повинен трактувати примітиви і об'єкти контейнери по-різному, навіть якщо більшість часу користувач поводиться з ними ідентично. Потреба в розрізнянні цих об'єктів робить програму більш складною. Шаблон Композитор описує як використовувати рекурсивну композицію, отож клієнтам не потрібно робити цю різницю.
Ключом до шаблону Композитор являється абстрактний клас, який представляє і примітиви і їхні контейнери. Для графічних систем, цей клас називається Graphic. Тип Graphic оголошує операції на подобі Draw, які специфічні для графічних об'єктів.
Він також оголошує операції, які розділяють усі композитні об'єкти, на подобі операцій для отримання доступу і управління їхніми потомками.
Дочірні класи Line, Rectangle і Text (перегляньте попередню діаграму класів) визначають примітиви графічних об'єктів. Ці класи реалізовують Draw для намалювання ліній, прямокутників і тексту відповідно. Оскільки графічні примітиви не мають графічних потомків, ніякі з цих потомків не реалізовують операції для управління потомками.
Клас Picture визначає накопичувач об'єктів Graphic. Тип Picture реалізовує Draw для викликання операції Draw з усіх її потомків, і вона реалізовує операції специфічні для управління потомками відповідно. Через те, що інтерфейс типу Picture відповідає інтерфейсу Graphic, об'єкти типу Picture може компонувати інші об'єкти Picture відповідно.
Наступна діаграма показує типову структуру компонованого об'єкту рекурсивно комонованих об'єктів Graphic:
Застосовуваність
Використовуйте шаблон Композитор коли
- ви бажаєте представити складні ієрархії об'єктів.
- ви бажаєте ігнорувати різницю між індивідуальними об'єктами і їх композиціями. Клієнти будуть трактувати усі об'єкти у композитній структурі уніфіковано.
Структура
Типова структура об'єкта Composite може виглядати наступним чином:
Учасники
- Component (Компонент — Graphic)
- оголошує інтерфейс для об'єктів у композиції
- реалізовує поведінку інтерфейсу за замовчуванням загальну для усіх класів у відповідності.
- оголошує інтерфейс для отримання доступу і управління його дочірніми компонентами.
- (опціонально) визначає інтерфейс для доступу до батьківського елемента у рекурсивній структурі, і реалізовує його якщо це необхідно.
- Leaf (Відгалуження — Rectangle, Line, Text, тощо)
- представляє об'єкти відгалуження у композиції. Елемент відгалуження немає потомків.
- Визначає поведінку для об'єктів-примітивів у композиції.
- Composite (Композиція — Picture)
- визначає поведінку для компонентів, які мають потомків
- зберігає дочірні компоненти
- реалізовує операції зв'язані з потомками у інтерфейсі типу Component.
- Client
- маніпулює об'єктами у композиції через інтерфейс типу Component.
Співпрацювання
Клієнти використовують інтерфейс класу Component для взаємодії об'єктами у композиційній структурі. Якщо реціпієнт являється типу Leaf, тоді запит обробляється безпосередньо. Якщо реціпієнт являється типом Composite, тоді він зазвичай передає запити до його потомків, можливо виконуючи додаткові операції перед чи/або після передавання.
Наслідки
Шаблон Композитор
- визначає ієрархію класів, яка складається з примітивних об'єктів і компонованих об'єктів. Примітивні об'єкти можуть бути компонованими у більш складні об'єкти, які у свою чергу також можуть бути рекурсивно компонованими і так далі. Кожного разу, коли клієнтський код очікує примітивний об'єкт, він також може працювати з компонованим об'єктом.
- робить клієнт простим. Клієнти можуть трактувати композитні структури і індивідуальні об'єкти уніфіковано. Клієнти зазвичай не знають (і не повинні знати) чи працюють вони з відгалуженням чи з композитним об'єктом. Це спрощує код клієнта через те, що шаблон запобігає написання функцій з case-вираженнями у класах які визначають композицію.
- полегшує додавання нових типів компонентів. Нові оголошені потомки типу Composite або Leaf працюють автоматично з інснуючими структурами і клієнтським кодом. Клієнти не повинні бути зміненими для нових класів типу Component.
- може зробити наш дизайн більш загальним. Недоліком легкого додавання нових компонентів полягає у тому, що це робить важчим обмеження компонентів композиції. Інколи ви бажаєте щоб композиція мала тільки певні компоненти. Через клас Composite, ви не можете полягатись на систему типів для отримання цих обмежень. Замість того, ви повинні будете використовувати перевірки під час виконання програми.
Реалізація
Існують багато проблем, які необхідно розглянути під час реалізовування шаблону Композитор:
- Явні зв'язки з батьками. Підтримка зв'язків від дочірніх компонентів до їхніх батьків може спростити перебір і управління композитною структурою. Батьківські зв'язки спрощують переміщення по структурі і видалення компоненту. Батьківські зв'язки також допомагають підтримку шаблону Ланцюг Відповідальностей.
Типовим місцем визначення батьківських зв'язків являється клас Component. Класи Leaf i Cmposite можуть успадкувати зв'язки і операції, які управляють ними.
Разом з батьківським зв'язком (вказівником на батьківський елемент), суттєвим являється підтримка інваріанти, що усі потомки композиції утримують, як і їхні батьки, композицію, яка у свою чергу утримує їх як дочірні елементи (двостороння деревоподібна структура). Найлегшим способом реалізації цієї інваріанти полягає у зміни батьківського елемента компоненту тільки коли він додається чи видаляється з композиції. Якщо це може бути реалізованим один раз у операціях Add i Remove класу Composite, тоді воно може бути успадкованим усіма дочірніми класами, і інваріанта буде підтримуватися автоматично.
- Спільні компоненти. Часто корисно розділяти компоненти, наприклад, для зменшення затрат на збереження. Але коли компонент може мати більше ніж один батьківський елемент, спільне використання компонентів стає складним.
Можливим рішенням для дочірніх елементів являється збереження множини батьківських зв'язків (вказівників на батьківські елементи). Але це може призвести до неоднозначностей, оскільки запити розповсюджуються структурою. Шаблон Легка Вага показує як перебудувати дизайн, щоб запобігти повного збереження батьків. Це працює у випадку, коли дочірні елементи можуть запобігти посилання запитів батьківських елементів втіленням декількох чи усіх їхніх станів.
- Мінімізація інтерфейсу Component. Однією з цілей шаблону Композитор є створення клієнтів, які не будуть розрізняти чи використовують вони певний клас-відгалуження, чи клас Composite. Щоб досягнути цю ціль, клас Component повинен визначити на стільки багато загальних операцій для класів Composite і Leaf, на скільки це можливо. Клас Component зазвичай постачає реалізацію за умовчанням для цих операцій і потомки класів Leaf i Composite будуть перевантажувати їх.
Однак, ця ціль інколи буде конфліктувати з принципом дизайну ієрархії класів, який твердить, що клас повинен визначати операції, які є змістовними до його потомків. Існують багато операцій, які підтримує тип Component і які не мають ніякого змісту для потомків типу Leaf. Як може Component постачати реалізацію за замовчуванням для них?
Інколи трохи креативності показує як операція, яка повинна з'явитися, має зміст тільки для типу Composite, і може бути реалізованою для усіх потомків типу Component, переміщуючи її до класу Component. Наприклад, інтерфейс для отримання доступу до потомків являється фундаментальною частиною класу Composite але не обов'язковою для класів Leaf. Але якщо ми поглянемо на тип Leaf як на Component який ніколи не матиме потомків, тоді ми можемо визначити операції за замовчуванням для отримання доступу до потомків у класі Component, які ніколи не повертають будь-яких потомків. Класи Leaf можуть використати реалізацію за замовчуванням, але класи Composite повинні реалізувати їх так, щоб вони повертали потомків елементу.
Операції управління потомками являються більш проблемними і будуть обговорюватися у наступному пункті.
- Оголошення операцій управління потомками. Хоч клас Composite реалізовує операції Add i Remove для управління потомками, важливим питанням у шаблоні Композитор являється те, що клас оголошує ці операції у ієрархії класів Composite. Чи повинні ми оголосити ці операції у класі Component і зробити їх змістовними для класів Leaf, чи може ми оголосимо і визначимо їх тільки у класів Composite і його потомках?
Рішення спричиняє компроміс між безпекою і прозорістю:
- Визначення інтерфейсу управління потомками в корінному класі ієрархії дає вам прозорість, оскільки ви можете трактувати усі компоненти уніфіковано. Однак, це коштує вам безпеки через те, що клієнти можуть намагатися виконати безглузді операції на подобі додавання і видалення об'єктів з відгалужень.
- Визначення управління потомками у класі Composite дає вам безпеку через те, що будь-яка спроба додати чи видалити об'єктів від відгалужень буде перехоплена під час компілювання у статично типізованих мовах на подобі С++. Але ви втрачаєте прозорість, тому що відгалуження і композити мають різні інтерфейси.
Ми виділяли прозорість над безпекою у цьому шаблоні. Якщо ви вибираєте безпеку, тоді іноді ви можете втратити інформацію про тип і будете змушені перетворювати компоненти у композити. Як ви можете зробити це без вдавання до небезпечних перетворень типів?
Один підхід полягає у оголошенні операції Composite* GetComposite() у класі Component. Тип Component забезпечує операцію за замовчуванням, яка повертає пустий (NULL) вказівник. Клас Composite перевантажує цю операцію так, щоб вона повертала вказівник на його тип:
class Composite;
class Component
{
public:
//...
virtual Composite* GetComposite() { return 0; }
} ;
class Composite : public Component
{
public:
void Add(Component*);
// ...
virtual Composite* GetComposite() { return this; }
} ;
class Leaf : public Component
{
// ...
} ;
Операція GetComposite() дозволяє вам опитувати компонент, щоб побачити чи являється він композитним об'єктом. Ви можете виконати Add i Remove безпечно з вказівника, який вона повертає.
Composite* aComposite = new Composite ;
Leaf* aLeaf = new Leaf ;
Component* aComponent ;
Composite* test ;
aComponent = aComposite ;
if (test = aComponent->GetComposite())
{
test->Add(new Leaf) ;
}
aComponent = aLeaf ;
if (test = aComponent->GetComposite())
{
test->Add(new Leaf) ; // не добавить відгалуження
}
Подібні тести для типу Composite можуть бути виконаними використовуючи конструкцію C++ dynamic_cast.
Звичайно, проблема тут полягає у тому, що ми не бажаємо трактувати усі компоненти уніфіковано. Ми повинні повернутися до тестування для інших типів перед тим, як обирати відповідну дію.
Єдиний спосіб забезпечити прозорість полягає у визначенні операцій за замовчуванням Add i Remove у класі Component. Це створює нову проблему: немає способу реалізувати операцію Component::Add без впровадження можливості невдачі її виконання. Ви можете так, щоб вона нічого не робила, але це ігнорує важливий аспект; тобто, спроба добавити що-небудь до класу відгалуження (leaf) ймовірно означає помилку в програмному забезпеченні. У цьому випадку, операція Add продукує сміття. Ви повинні заставити видаляти її аргумент, але це може бути не цією поведінкою, яку очікую клієнт.
Зазвичай краще зробити так, щоб операції Add i Remove провалювали виконання за умовчанням (можливо генеруванням виключення), якщо компоненту не дозволено мати потомків або аргумент операції Remove не являється потомком компоненту, відповідно.
Іншою альтернативою являється невелика зміна значення “видалення”. Якщо компонент підтримує вказівник на батьківський елемент, тоді ми можемо перевантажити Component::Remove для видалення самого себе від його батьківського елементу. Однак, все-одно немає змістовної інтерпретації для відповідної операції Add.
- Чи повинен Component реалізувати список Components? Ви можете забажати визначити множину потомків як примірник змінної в класі Component, де визначаються операції до їхнього доступу і управління. Але вкладання вказівників на потомки у базовий клас зазнає штрафу в розмірі для кожного відгалуження, навіть якщо відгалуження ніколи не матиме потомків. Це дає результат тільки, якщо існує відносно невелика кількість потомків у структурі.
- Порядок потомків. Велика кількість дизайнів визначають порядок потомків у Composite. У попередньому прикладі Graphics, порядок відображає порядок front-to-back (спереду-назад). Якщо тип Composite представляє дерева аналізу, тоді сполучення виражень можуть бути примірниками типу Composite, чиї потомки повинні бути впорядкованими для відображення програми.
Коли порядок потомків являється важливим чинником, ви повинні обережно спроектувати інтерфейс доступу до потомків і їх управління для того що управляти послідовностями потомків. Шаблон Ітератор може направляти вас у цьому.
- Кешування для покращення виконання. Якщо ви потребуєте часто перебирати чи виконувати пошук у композиції, клас Composite може кешувати інформацію про перебір чи пошук про його потомки. Композитор може кешувати фактичні результати чи просто інформацію, яка дозволяє зменшити наступний перебір чи пошук. Наприклад, клас Picture з прикладу секції Мотивація може кешувати обмежуючу площу для його потомків. Під час відмальовування чи вибору, ця кешована обмежуюча область дозволяє типу Picture запобігти відмальовування чи пошук під час того, як його потомки невидимі у поточному вікні.
Зміни до компоненту вимагатимуть оновлення кешу його батьків. Це найкраще працює коли компонентам відомі їхні батьки. Отож, якщо ви використовуєте кешування, вам необхідно визначити інтерфейс для вказування композиціям, що їхній кеш являється не достовірним.
- Хто повинен видаляти компоненти? У мовах, які не мають механізм збирання сміття (garbage collector), зазвичай найкращим являється надавання відповідальності за видалення потомків для типу Composite, при виклику його деструктора. Виняток для цього правила полягає у тому, коли об'єкти типу Leaf являється незмінними і тому можуть бути розділеними між іншими об'єктами.
- Яка найкраща структура даних для збереження компонентів? Композиції можуть використовувати різні структури даних для збереження їхніх потомків, включаючи зв'язані списки, дерева, масиви і хеш-таблиці. Вибір структури даних залежить (як завжди) від ефективності. Фактично, взагалі необов'язково використовувати структури даних загального призначення. Інколи композити мають змінну для кожного потомка, хоча це вимагає реалізування у кожному дочірньому класі Composite його власного інтерфейсу управління потомками. Перегляньте шаблон Інтепретатор, наприклад.
Приклад Коду
Устаткування на подобі комп'ютерів і стерео компонентів часто організовуються у вміщувальні ієрархії. Наприклад, шасі може мітити приводи і планарні дошки, шина може містити карти, і корпус може містити шасі, шини тощо. Такі структури можуть бути натурально змодельованими за допомогою шаблона Композитор.
Клас Equipment визначає інтерфейс для усього устаткування у ієрархії (part-whole?).
class Equipment
{
public:
virtual ~Equipment() ;
const char* Name() { return _name; }
virtual Watt Power() ;
virtual Currency NetPrice() ;
virtual Currency DiscountPrice() ;
virtual void Add(Equipment*) ;
virtual void Remove(Equipment*) ;
virtual Iterator* CreateIterator() ;
protected:
Equipment(const char*) ;
private:
const char* _name;
} ;
Тип Equipment оголошує операції, які повертають атрибути частини обладнання, на подобі його споживання енергії і ціни. Потомки реалізовують ці операції для специфічних типів устаткування. Тип Equipment також визначає операцію CreateIterator, яка повертає тип Iterator (перегляньте Додаток В) для отримання доступу до його частин. Реалізація за замовчуванням для цих операцій повертає NullIterator, який виконує перебір на пустій множині.
Потомки типу Equipment можуть включати класи типу Leaf, які представляють дискові приводи, інтегральні схеми і вимикачі:
class FloppyDisk : public Equipment
{
public:
FloppyDisk(const char*) ;
virtual ~FloppyDisk() ;
virtual Watt Power() ;
virtual Currency NetPrice() ;
virtual Currency DiscountPrice() ;
} ;
Тип CompositeEquipment являється базовим класом для устаткування, яке містить інші його частини. Він являється також потомком типу Equipment.
class CompositeEquipment : public Equipment
{
public:
virtual ~CompositeEquipment() ;
virtual Watt Power() ;
virtual Currency NetPrice() ;
virtual Currency DiscountPrice() ;
virtual void Add(Equipment*) ;
virtual void Remove(Equipment*) ;
virtual Iterator* CreateIterator() ;
protected:
CompositeEquipment(const char*) ;
private:
List _equipment ;
} ;
Тип CompositeEquipment визначає операції для отримання доступу і управління частинами устаткування. Операції Add і Remove додають і видаляють устаткування з списку устаткування, який зберігається у полі _equipment. Операція CreateIterator повертає ітератор (особливо, примірник ListIterator), який перебирає цей список.
Реалізація за замовчуванням операції NetPrice може використовувати CreateIterator для сумування фабричних цін усіх частин устаткування
2:
Currency CompositeEquipment :: NetPrice ()
{
Iterator* i = CreateIterator() ;
Currency total = 0 ;
for (i->First(); !i->IsDone(); i->Next())
{
total += i->CurrentItem()->NetPrice() ;
}
delete i ;
return total ;
}
Тепер ми можемо представити комп'ютерне шасі як потомок CompositeEquipment, який називається Chassis. Тип Chassis успадковує операції для управління потомками від CompositeEquipment.
class Chassis : public CompositeEquipment
{
public:
Chassis(const char*) ;
virtual ~Chassis() ;
virtual Watt Power() ;
virtual Currency NetPrice() ;
virtual Currency DiscountPrice() ;
} ;
Ми можемо визначити схожим способом інші контейнери устаткування на подобі Cabinet і Bus. Це дає нам все, що нам необхідно для складання устаткування у (доволі простий) персональний комп'ютер:
Cabinet* cabinet = new Cabinet("PC Cabinet") ;
Chassis* chassis = new Chassis("PC Chassis") ;
cabinet->Add(chassis) ;
Bus* bus = new Bus("MCA Bus") ;
bus->Add(new Card("16Mbs Token Ring")) ;
chassis->Add(bus);
chassis->Add(new FloppyDisk("3.5in Floppy")) ;
cout << "The net price is " << chassis->NetPrice() << endl ;
Відомі використання
Приклад шаблону Композитор можуть бути знайдені у майже усіх об'єктно-орієнтованих системах. Оригінальний клас View з системи Model/View/Controller Smalltalk [KP88] являється композитором, і майже кожний інструментарій інтерфейсу користувача чи фреймворк слідує його кроками, включаючи ET++ (разом з його Vobject [WGM88]) і InterViews (Styles[LCI+92]), Graphics [VL88] і Glyphs [CL90]. Цікаво спостерігати, що оригінальний тип View з системи Model/View/Controller має набір підтипів, іншими словами, View являється і класом Component, і класом Composite. Випуск 4.0 системи Smalltalk-80 виправив Model/View/Controller за допомогою класу VisualComponent, який створює потомки View і CompositeView.
Фреймворк компілятора RTL Smalltalk [JML92] широко використовує шаблон Композитор. RTLExpression являється класом Component для дерев аналізу. Він має потомки на подобі BinaryExpression, які містять об'єкти потомки RTLExpression. Ці класи визначають композитні структури для дерев аналізу. Тип RegisterTransfer являється класом Component для проміжної форми Signle Static Assignment (SSA) програми. Потомки відгалуження типу RegisterTransfer визначають статичні присвоєння на подобі
- примітивні присвоєння, які виконують операцію на двох регістрах і присвоюють результат до третього;
- як присвоєння від джерельного регістру, але без цільового регістру, що означає, що регістр використовується після того, як виконається підпрограма; і
- присвоєння з цільовим регістром але без джерельного, що означає що регістру присвоюється значення перед виконанням підпрограми.
Інший потомок, RegisterTransferSet, являється класом Composite для представлення присвоєнь, які змінюють декілька регістрів за один раз.
Іншим прикладом цього шаблону з'являється у фінансовій сфері, де портфоліо накопичує індивідуальні активи. Ви можете підтримувати складні накопичення активів, реалізуванням портфоліо як тип Composite, який відповідає інтерфейсу індивідуальних активів [BE93].
Шаблон Команда описує як об'єкт Command може бути компонованим і доданим в ряд за допомогою класу-композитора MicroCommand.
Споріднені шаблони
Часто компонент — батьківський зв'язок використовується для Ланцюга Відповідальностей.
Декоратор часто використовується з Композитором. Коли декоратор і композитор використовуються разом, вони зазвичай мають загальний батьківський клас. Отож декоратори будуть підтримувати інтерфейс типу Component за допомогою операцій на подобі Add, Remove і GetChild.
Легка Вага дозволяє вам розділяти компоненти, але вони можуть більше не відноситись до їхніх батьків.
Ітератор може бути використаним для перебору композицій.
Відвідувач локалізує операції і поведінку, яка в іншому випадку буде поширеною між класами Composite і Leaf.
2 Легко забути видалити ітератор, коли ви закінчили використовувати його. Шаблон Iterator показує як пильнувати такі помилки.