10 серпня 2010 р.

"Патерн Вівторка" #2: Декоратор

Розкажу я вам про лікаря, який мав хорошу й швидку машину - Mersedes. Оскільки він лікар Львівської лікарні, то по дорозі на роботу він часто застрягає у пробках, тому він спізнюється і це його виводить із себе, в результаті чого страждають... Хто? - Його пацієнти. В нашого лікаря є мрія, що його машина перетворилася на Швидку і всі інші автомобілі уступаються йому дорогу на роботу. Тільки одна проблема із цим: Швидка вміє сигналити сиреною, а його мерс не має такої можливості, нам, як програмістам, було б добре ДЕКОРУВАТИ (decorate) мерс, таким чином, щоб він почав голосно "вити".
Як ми це можемо зробити?

ДЕКОРАТОР

Отже, маємо базовий клас Автомобіля (Car):

    public class Car
    {
        protected String BrandName { get; set; }

        public virtual void Go()
        {
            Console.WriteLine("I'm " + BrandName + " and I'm on my way...");
        }
    }

і конкретна імплементація - Mersedes:

    public class Mersedes : Car
    {
        public Mersedes()
        {
            BrandName = "Mersedes";
        }
    }

Паттерн Декоратор використовується для надання деякої додаткової функціональності нашим об'єктам. В нашому прикладі, ми хочемо додати функціональність сирени до конкретної імплементації автомобіля, проте нам нічого не заважає причепити на машину газовий розпилювач абощо. Для того щоб зберегти контракт базового класу Car, і щоб мати базовий клас для всіх інших "прибамбасних" функціональностей створимо CarDecorator, що так само наслідується від Car:

    public class DecoratorCar : Car
    {
        protected Car DecoratedCar { get; set; }

        public DecoratorCar(Car decoratedCar)
        {
            DecoratedCar = decoratedCar;
        }

        public override void Go()
        {
            DecoratedCar.Go();
        }
    }

Як ми уже зауважили цей клас обгортує DecoratedCar. Це є причина чому цей патерн ще часто називають Wrapper.

Настав час для "прибамбасів". Ми додаємо додаткову функціональність до будь-якої машини (чи то мерс чи то беха), наслідуючися від CarDecorator класу.

    public class AmbulanceCar : DecoratorCar
    {
        public AmbulanceCar(Car decoratedCar)
            : base(decoratedCar)
        {
        }

        public override void Go()
        {
            base.Go();
            Console.WriteLine("... beep-beep-beeeeeep ...");
        }
    }

Тут ми додали простий екстеншин біпкання.

І власне те чого ми добивалися:


Викорисання вигладає дуже приємно - ми покриваємо Мерседс фічами Швидкої і отримуємо мрію лікаря (doctorsDream), опісля ми можемо покрити цей об'єкт ще єкимись штукенціми, і що важливо ми це все можемо робити динамічно. Сниться лікарю* біпкалка - передаємо його мерс у декоратор Ambulance, сниться йому кулемет на криші автомобіля, передаємо у декоратор Armor.

            var doctorsDream = new AmbulanceCar(new Mersedes());
            doctorsDream.Go();

Вивід:

I'm Mersedes and I'm on my way...
... beep-beep-beeeeeep ...

Надіюся що цей аутпут був очікуваний. А тепер швидко глянемо на UML цієї мудрості-чудасії:


Цей патерн має щось спільне із Композитом (Composite) і Адаптером (Adapter). Адаптер може змінити інтерфейс поведінки, а Декоратор ні (ми наслідуємося від Car). Композит працює із великою кількістю компонент, а не як декоратор тільки із однією.


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

(*) раніше було "сниться дікарю біпкалка", стало "сниться лікарю біпкалака".

12 коментарів:

  1. Надзвичайно просто та зрозуміло. Розжували все, залишилося лише ковтнути.. :)

    ВідповістиВидалити
  2. Описав з фантазією, це плюс. "Сниться дікарю біпкалка" мене трохи поперло, думаю це чепятка ))
    А так все гуд, коротко та зрозуміло, так тримати.

    ВідповістиВидалити
  3. Ігор, дякую, я справив. Мабуть то чепятка тому, що я вже сонний добре був :)

    ВідповістиВидалити
  4. Ага, доречі на моєму блозі вже є декілька патернів описаних подібним або околоподібним стилем, але чомусь ніхто їх так високо не оцінює як ви, за винятком декількох друзів. (Може просто ніхто не читав ще :) )

    ВідповістиВидалити
  5. Я правда не зрозумів для чого DecoratorCar має бути нащадком Car. Хтось пояснить доступно?:)

    ВідповістиВидалити
  6. Дмитро, щоб зберегти той самий контракт. (Я це був вказав "Для того щоб зберегти контракт базового класу Car, і щоб мати базовий клас для всіх інших "прибамбасних" функціональностей створимо DecoratorCar, що так само наслідується від Car")

    Ну може краще для прикладу було написати ICar тоді було б чіткіше ясно що то контракт.

    ВідповістиВидалити
  7. Чи ти із мене просто прикалуєшся? :) Інакше б цей патерн не називався б враппер, якщо б він не мав тих самих методів, які він враппає.

    ВідповістиВидалити
  8. Класний паттерн. Один з моїх улюблених. І написано доступно, тільки мені здається що на UML діаграмі скорше зображений брідж ніж декоратор.
    Також на мою думку якщо потрібний лише один декоратор то можна обійтися без базового класу (CarDecorator) і реалізувати зразу ж (AmbulanceCar).

    ВідповістиВидалити
  9. Можна було обійтися тільки із одним декоратором. А на UML таки не брідж зображений, але певно ще бракує стрілки, щоб зобразити, що CarDecorator також наслідується від Car.

    ВідповістиВидалити