Намір
Динамічно прикріпляє додаткову відповідальність до об'єкта. Дкоратор забезпечує гнучку альтернативу до створення потомків для розширення функціональності.
Також відомий як
Wrapper — Обгортувач
Мотивація
Інколи ми бажаємо додати відповідальності до індивідуальних об'єктів, а не до усього класу. Інструментарій графічного інтерфейсу користувача, наприклад, повинен дозволяти вам додавати властивості на подобі границь або поведінку на подобі здатності прокручуватися до будь-якого компоненту інтерфейсу користувача.
Одним способом додавання відповідальностей полягає в успадковуванні. Успадковування межі від іншого класу вкладає обрамлення межею навколо кожного примірника потомка. Це не гнучко, адже вибір обрамлення зроблено статично. Клієнт не можу контролювати як і коли декорувати компонент обрамленням.
Більш гнучким підходом являється обгородження компонента у інший об'єкт, який додає обрамлення. Цей обгороджувальний об'єкт називається декоратор. Декоратор відповідає інтерфейсу компонента, який він декорує, отож його присутність являється прозорою для клієнтів компоненту. Декоратор передає запити до компонента і може виконувати додаткові дії (на подобі відмальовування межі) перед тим чи після передачі. Прозорість дозволяє вам рекурсивно вкладати декоратори, в зв'язку з чим, дозволяти необмежену кількість додаткових відповідальностей.
Наприклад, припустимо, що ми маємо об'єкт TextView, який відображає текст у вікні. Тип TextView немає смуг прокручування за замовчуванням через те, що нам не завжди вони потрібні. Але коли вони необхідні, для їх додавання ми можемо використати ScollDecorator. Припустимо, що ми також бажаємо добавити товсту чорну межу навколо елемента TextView. Ми так само можемо використати BorderDecorator для їх додавання. Ми просто компонуємо декорації з TextView для отримання бажаного результату.
Наступна діаграма об'єктів показує як компонувати об'єкт TextView за допомогою об'єктів BorderDecorator і ScrollDecorator для отримання елемента перегляду тексту з межею і смугами прокрутки:
Класи ScrollDecorator i BorderDecorator являється потомками класу Decorator, абстрактний клас для візуальних компонентів, які декорують інші візуальні компоненти.
VisualComponent являється абстрактним класом для візуальних об'єктів. Він визначає їхній інтерфейс для відмалювання і опрацювання подій. Зверніть увагу на те, як клас Decorator просто передає запити на вілмальовування до його компонентів і як потомки Decorator можуть розширювати цю операцію.
Потомки Decorator являються можуть вільно додавати операції для специфічної функціональності. Наприклад, операція ScrollTo типу ScrollDecorator дозволяє іншим об'єктам прогорнути інтерфейс якщо вони знають, що в інтерфейсі об'єкт являється примірником типу ScrollDecorator. Важливим аспектом цього шаблону являється те, що він дозволяє декораторам з'являться будь-де, де може існувати VisualComponent. Таким чином клієнти в загальному не можуть визначити різницю між декорованим компонентом і не декорованим, і отож вони взагалі не залежить від декорацій.
Застосовуваність
Використовуйте декоратор коли
- динамічно і прозоро додаєте відповідальності до індивідуальних об'єктів, тобто без впливу на інші об'єкти.
- для відповідальностей які можуть бути відокремленими.
- коли розширення створенням потомків являється непрактичним. Інколи можлива велика кількість незалежних розширень і це буде продукувати вибух потомків, для підтримки кожної комбінації.
- Або визначення класу може бути прихованим або в іншому випадку недоступним для створення потомків.
Структура
Учасники
- Component (Компонент — VisualComponent)
- Визначає інтерфейс для об'єктів, які можуть мати відповідальності, додані до них динамічно.
-
ConcreteComponent (КонкретнийКомпонент — TextView)
- визначає об'єкт до якого можуть бути прикріпленими додаткові відповідальності.
- Decorator (Декоратор)
- підтримує вказівник до об'єкту типу Component і визначає інтерфейс, який відповідає до інтерфейсу Component.
- ConcreteDecorator (КонкретнийДекоратор — BorderDecorator, ScrollDecorator)
- додає відповідальності до компоненту.
Співпрацювання
Декоратор передає запити до його об'єкту Component. Він необов'язково може виконувати додаткові операції перед тим чи після передавання запиту
Наслідки
Шаблон Декоратор має хоча б дві ключові переваги і два недоліки:
- Більша гнучкість у порівнянні з статичним успадкуванням. Шаблон Декоратор забезпечує більш гнучкий спосіб додавання відповідальностей до об'єктів, ніж це можливо при статичному (множинному) успадковуванні. За допомогою декораторів, відповідальності можуть бути добавленими чи видаленими під час виконання, просто їхнім приєднанням чи від'єднанням. На противагу успадковуванню, яке вимагає створення нових класів для кожної додаткової відповідальності (наприклад BorderedScrollableTextView, BorderedTextView). Це спричиняє створення багатьох класів і збільшує складність системи. Більше того, забезпечення інших класів Decorator для певних класів Component, дозволяє вам змішувати і з'єднувати відповідальності.
Декоратори також полегшують дворазове додавання властивості. Наприклад, для додавання до TextView подвійної межі, просто приєднайте два примірника типу BorderDecorators. Дворазове успадковування від класу Border в кращому випадку призведе до помилок.
- Запобігання виникненню функціонально перевантажених класів у ієрархії. Декоратор пропонує підхід “сплати по використанню” (pay-as-you-go) відносно з точки зору додавання функціональності. Замість того, щоб намагатися підтримати усі передбачувані можливості у складному громіздкому класі, ви можете визначити простий клас і поступово додавати функціональність за допомогою об'єктів-декораторів. Функціональність може бути скомпонованою з простих частин. І як результат, програмі не потрібно розплачуватися за функції, які вона не використовує. Також легко визначити нові типи декоратора незалежно від класів об'єктів, які вони розширюють, навіть для непередбачених можливостей. Розширювання складних класів спричиняє виникненню деталей, які не відносяться до відповідальностей, які ви додаєте.
- Декоратор і його компоненти не ідентичні. Декоратор поводить себе як прозора обгортка. Але з точки зору об'єктної ідентичності, декоровані компоненти не ідентичні до самого компоненту. Отже ви не повинні довіряти об'єктній ідентичності під час використання декораторів.
- Велика кількість малих об'єктів. Дизайн, який використовує Декоратор часто отримує систему, компоновану з великої кількості малих об'єктів, які виглядають подібними один на одного. Об'єкти відрізняються тільки у способі, яким вони зв'язані, а не їхнім класом чи значенням їхніх внутрішніх змінних. Хоча ці системи легко налаштувати тими, хто їх розуміє, вони можуть бути важкими у вивченні і відлагоджені.
Реалізація
Необхідно розглянути декілька питань під час застосування шаблону Декоратор:
- Узгодження інтерфейсу. Інтерфейс об'єктів декораторів повинен відповідати до інтерфейсу компоненту, які вони декорують. Отже класи ConcreteDecorator повинні успадковувати від загального класу (хоча б у С++).
- Нехтування абстрактним класом Decorator. Немає необхідності у визначенні абстрактного класу Decorator, коли вам потрібно додавати одну відповідальність. Це часто являється випадком коли ви скоріше маєте справу з існуючими ієрархіями класів, ніж повинні створити нову. У цьому випадку, ви можете з'єднати відповідальності Декоратора для передавання запитів з компонентами у клас ConcreteDecorator.
- Утримування легкими класи Component. Щоб забезпечити сумісний інтерфейс, компоненти і декоратори повинні бути похідними від загального класу Component. Важливо утримувати цей загальний інтерфейс класу легким; тобто, потрібно сфокусуватися на визначенні інтерфейсу, а не на збережені даних. Визначення представлення даних повинне бути відкладене для дочірніх класів; у іншому випадку складність класу Component може зробити декоратори занадто важкими для використання в великих кількостях. Вкладання великої функціональності у тип Component також збільшує можливість використання додаткових ресурсів потомками через функції, які їм не потрібні.
- Зміна шкіри об'єкта на противагу зміни його нутрощів. Ми можемо думати про декоратор, як про шкіру навколо об'єкта, яка змінює його поведінку. Альтернатива полягає у зміні нутрощів об'єкта. Шаблон Стратегія являється хорошим прикладом шаблону для зміни нутрощів об'єкту.
Стратегії являється кращим вибором у ситуаціях де класу Component притаманна обтяженість функціями, і тому застосування шаблону Декоратор обходиться занадто дорого. У шаблоні Стратегія, компонент передає деяку свою поведінку до окремого об'єкту стратегія. Шаблон Стратегія дозволяє змінювати чи розширювати функціональність компоненту, замінюючи об'єкт стратегії.
Наприклад, ми можемо підтримувати різні стилі обрамлення для різних об'єктів Border. Об'єкт Border являється об'єктом стратегією, яка інкапсулює стратегію відмальовування обрамлення. Розширенням числа стратегій від однієї до необмеженої кількості, ми отримаємо той сами ефект, як від рекурсивного упаковування декорацій.
У MacApp 3.0 [App89] і Bedrock [Sym93a], наприклад, графічні компоненти (названі “views” - “перегляди”) підтримують список “оздоблюючі” (“adorner”) об'єкти, які можуть прикріпити додаткові оздоби на подобі обрамлень до об'єктів переглядів. Якщо перегляд має прикріплені які-небудь оздоблюючі об'єкти, в такому разі він дає їм шанс відмалювати додаткові оздоби. MacApp і Bedrock повинні використовувати цей підхід тому, що клас View являється обтяженим. Використання повністю розвинутого типу View для додавання одного обрамлення буде занадто дорогим з точки зору ресурсів.
Оскільки шаблон Декоратор змінює тільки компонент з-зовні, компонент не має знати будь-що про його декорації; тобто, декорації являються прозорими для компоненту:
За допомогою стратегій, компонент усвідомлений про можливі декорації. Отож він має зв'язувати і підтримувати відповідні стратегії:
Підхід, заснований на стратегіях, може вимагати модифікування компоненту для пристосування нових розширень. З іншої сторони, стратегія може мати свої власні спеціалізовані інтерфейси, коли інтерфейс декораторів повинен відповідати до інтерфейсу компонентів. Стратегія для відображення межі, наприклад, потребує визначення тільки інтерфейсу для відображення межі (DrawBorder, GetWidth тощо), що означає, що стратегія може бути необтяженою навіть, якщо клас Component являється перевантаженим.
MacApp і Bedrock використовують цей підхід для більше ніж простого оздоблення переглядів. Вони також використовують його для нарощення поведінки обробки подій об'єктів. У обох системах, перегляд (view) підтримує список “поведінки” об'єктів, які можуть модифікувати і перехоплювати події. Перегляд дає кожному з реєстрованих об'єктів поведінки шанс обробити подію перед незареєстрованими об'єктами, ефективно заміщуючи їх. Ви можете декорувати перегляди за допомогою спеціальної підтримки обробки клавіатурних подій, наприклад, реєструючи об'єкт поведінки, який перехоплює і обробляє події натискання клавіш.
Приклад Коду
Наступний код показує як реалізовувати декорації інтерфейсу користувача у С++. Ми припустимо, що існує клас Component, який називається VisualComponent.
class VisualComponent
{
public:
VisualComponent();
virtual void Draw();
virtual void Resize();
// ...
} ;
Ми визначаємо потомок класу VisualComponent, який називається Decorator, для якого ми створимо потомки, щоб підтримувати різні декорації.
class Decorator : public VisualComponent
{
public:
Decorator(VisualComponent*) ;
virtual void Draw() ;
virtual void Resize() ;
// ...
private:
VisualComponent* _component ;
} ;
Декоратор декорує VisualComponent, утримуючи вказівник _component внутрішньою змінною, яка ініціалізовується в конструкторі. Для кожної операції у інтерфейсі VisualComponent, Decorator визначає реалізацію за замовчуванням, яка передає запити далі до _component:
void Decorator :: Draw ()
{
_component->Draw() ;
}
void Decorator :: Resize ()
{
_component->Resize() ;
}
Потомки класу Decorator визначають специфічні декорації. Наприклад, клас BorderDecorator додає межу до компонента, який він обгортає. BorderDecorator являється потомком Decorator, який перевантажує операцію Draw для відмальовування межі. BorderDecoration також визначає приватну допоміжну операцію DrawBorder, яка виконує відмальовування. Потомок успадковує усі інші реалізації операцій від Decorator.
class BorderDecorator : public Decorator
{
public:
BorderDecorator(VisualComponent*, int borderWidth) ;
virtual void Draw() ;
private:
void DrawBorder(int) ;
private:
int _width ;
} ;
void BorderDecorator :: Draw ()
{
Decorator::Draw() ;
DrawBorder(_width) ;
}
Подібної реалізації будемо дотримуватися для ScrollDecorator i DropShadowDecoraor, які будуть додавати підтримку прокручування і відкидання тіні для візуального компоненту.
Тепер ми можемо компонувати примірники цих класів для забезпечення різноманітних декорацій. Наступний код ілюструє як ми можемо використовувати декорації, щоб створити обрамлений і прокручуваний TextView.
Спочатку, нам потрібно спосіб вкласти візуальні компоненти у віконний об'єкт. Ми припустимо, що наше клас Window постачає операцію SetContents для цих цілей:
void Window :: SetContents (VisualComponent* contents)
{
// ...
}
Тепер ми можемо створити переглядач тексту і його вікно:
Window* window = new Window ;
TextView* textView = new TextView ;
TextView являється VisualComponent, який дозволяє нам вкладати його у вікно:
window->SetContents (textView) ;
Але ми бажаємо отримати обрамлений і прокручуваний примірник TextView. Отож ми декоруємо його перед тим, як вкладати його у вікно.
window->SetContents(new BorderDecorator(
new ScrollDecorator(textView),
1
)
) ;
Через те що вікно отримує доступ до його контенту через інтерфейс VisualComponent, він не підозрює про присутність декораторів. Ви, як клієнт, можете відслідковувати переглядач тексту, якщо ви повинні співпрацювати з ним напряму, наприклад, коли вам необхідно викликати операції, які не є частинами інтерфейсу VisualComponent. Клієнти які полягаються на ідентичності компоненту повинні так само відноситись до нього безпосередньо.
Відомі Використання
Велика кількість об'єктно-орієнтованих інструментаріїв інтерфейсу користувача використовують декоратори для додавання графічних прикрас до віджетів. Прикладами являються IterViews [LVC98, LCI+92], ET++[WGM88] і бібліотека класів ObjectWorks\Smalltalk [Par90]. Більш екзотичними програмами з використанням Декоратора являються DebuggingGlyph з InterViews і PassivityWraper з ParcPlace Smalltalk. Клас DebuggingGlyph видруковує відлагоджувальну інформацію перед і після передавання запиту до його компоненту. Ця інформація може бути використаною для аналізу і відлагоджування планової поведінки об'єктів у комплексних композиціях. PassivityWrapper може увімкнути чи вимкнути взаємодію з користувачем і компонента. Але шаблон Декоратор не є обмежений використанням у графічному інтерфейсі користувача, як це ілюструє наступний приклад (заснований на потокових класах ET++ [WGM88]).
Потоки являються фундаментальними абстракціями у більшості можливостях вводу/виводу. Потік може забезпечувати інтерфейс для перетворення об'єктів у послідовності байтів чи букв. Це дозволяє нам переписувати об'єкти у файли чи у символьні рядки в пам'яті для пізнішого використання. Прямолінійний спосіб їх виконання полягає у визначенні абстрактного класу Stream з потомками MemoryStream i FileStream. Але припустимо, що ми також бажаємо виконувати наступне:
Стискати дані потоку використовуючи різні алгоритми компресії (Лампель-Зів тощо).
Перетворювати дані потоку у семи-бітні букви ASCII, отож вони можуть бути переданими через канал зв'язку ASCII.
Шаблон декоратор дає нам елегантний спосіб додавання цих відповідальностей до потоків. Діаграма, розміщена нижче, показує одне рішення проблеми:
Абстрактний клас Stream обробляє внутрішній буфер і забезпечує операції для збереження даних у потік (PutInt, PutString). Кожного разу, коли буфер переповнений, Strean викликає абстрактну операцію HandleBufferFull(), яка виконує безпосередню передачу даних. Версія цієї операції для FileStream перевантажує її для передачі буфера у файл.
Ключовим класом тут являється StreamDecorator, який підтримує зв'язок з потоком-компонентом і передає запити до нього. Потомки StreamDecorator перевизначають HandleBufferFull і виконують додаткові дії перед викликом операції HandleBufferFull з типу StreamDecorator.
Наприклад, потомок CompressingStream стискає дані ASCII7Stream перетворює дані у семи-бітний ASCII. Тепер, для створення FileStream, який стискає свої дані і перетворює стиснуті бінарні дані у семи-бітний ASCII, ми декоруємо FileStream за допомогою CompressingSream і ASCII7Stream:
Stream* aStream = new CompressingStream(
new ASCII7Stream(
new FileStream("aFileName")
)
);
aStream->PutInt(12) ;
aStream->PutString("aString") ;
Споріднені Шаблони
Адаптер: декоратор відрізняються від адаптера у тому, що декоратор змінює тільки відповідальності об'єкта, а не його інтерфейс; адаптер надасть об'єкту зовсім новий інтерфейс.
Композитор: Декоратор можна розглядати з точки зору дегенеративного композитора, тільки з одним компонентом. Однак, декоратор додає додаткові відповідальності — він не призначався для накопичення об'єктів.
Стратегія: Декоратор дозволяє вам змінювати шкіру об'єкта; стратегія дозволяє змінювати його нутрощі. Це два альтернативних способи зміни об'єкта.