Въпреки че дизайнът на декоратора не се смята за един от „най-важните дизайнерски модели“, които трябва да се овладеят като програмист, определено се забавлявах много да го науча поради особения ефект, който създава, когато се внедри. Тази статия ще се различава малко от други статии в тази поредица като Factory Method, Builder, Adapter Design Pattern, където се опитвам да дефинирам стриктно по-всеобхватното изпълнение на тези привидно ясни дизайнерски модели. По-скоро просто ще споделя моите поуки от изучаването на модела на декоратора на Refactoring Guru, като използвам пример в C++.

Дизайнът на декоратора е креативен модел, който позволява подобряването на интерфейсите. Подобреният обект формира структура, подобна на стек, със слоеве и слоеве от допълнителни атрибути, добавени следвайки принципите „Последен в първи излязъл“ (LIFO). Когато се използва декорираният обект, всеки слой и всеки добавен атрибут се разширяват рекурсивно, образувайки поведение, подобно на руска вложена кукла. Наистина мисля, че е завладяваща концепцията за подобряване и разширяване на обект чрез обвиване на декоратор отгоре, но в основата основните характеристики на обвития обект не са се променили.

Да предположим, че изграждаме магазин за сандвичи, където потребителят има възможност да избере основата на сандвича, след което да добави гарнитури и подправки към него. Основата на сандвича има базов вариант, който използва обикновен хляб и месо, а като луксозна версия използва органичен хляб и месо, заедно със сирене и маруля в него. След като клиентът избере основата и избере допълнителните гарнитури и подправки, магазинът за сандвичи ще върне общата цена и различните съставки, които са влезли в сандвича.

Отправна точка за проектиране на този клас би било да има базов клас Sandwich и да го разширите в луксозни и основни дъщерни класове Sandwich. Очевидно не бихме искали да продължим да извличаме повече подкласове за различните страни и подправки, в противен случай броят на подкласовете просто ще продължи да се увеличава. Това е моментът, когато принципът „предпочитайте композицията пред наследството“ влиза в действие.

Вместо това, по-добър подход би бил да се разширят класовете Basic или Deluxe Sandwich чрез добавяне на атрибути към тях. Например класът Sandwich има контейнерно поле, наречено Sides, и друго, наречено Condiments. Това работи добре, но добавянето или премахването на компоненти от класа изисква работа с контейнера, което го прави по-малко гъвкав и разширяем и може би по-малко елегантен.

Основната идея на декоратора е, че можем да използваме този клас за подобряване на класовете Sandwich. Можем да имаме CondimentDecorator и SideDecorator, които ще се използват за обвиване на класове сандвичи, за да подобрим и разширим функционалността му. Следващата графика сравнява двата подхода.

Преди да навлезем в конкретната реализация, нека се уверим, че разбираме дефинициите и UML на модела Decorator. Според „Refactoring Guru“:

Декоратор е модел на структурно проектиране, който ви позволява да прикрепите нови поведения към обекти, като поставите тези обекти в специални обекти-обвивки, които съдържат поведенията.

Идеята е, че новото поведение е украсено чрез опаковане на стари предмети в него. Освен това, за клиентския код, извикването на декориран обект или оригинален обект не трябва да има значение.

Интерфейсът на компонента е това, с което клиентът взаимодейства, а конкретните компоненти реализират интерфейса. В нашия пример за магазин за сандвичи сандвичът е компонентът, а бетонните компоненти са основните и луксозните сандвичи. Това са градивните елементи, с които ще започнем.

Базовият декоратор също имплементира компонентния интерфейс. Което може да е странно за визуализиране, защото защо CondimentDecorator би внедрил сандвича. Всичко това ще има смисъл след секунда. И накрая, ConcreteDecorators са специфичните декоратори, които искаме да използваме за декориране на бетонния компонент, като CondimentDecorator или SideDecorator.

Ще забележите, че BaseDecorator не само имплементира Component Interface, но също така има екземпляр на Component (Aggregation). Инстанцията на компонента е това, което декораторът обвива вътре.

Първо ще внедрим компонентния интерфейс и бетонните компоненти в нашия магазин за сандвичи:

class SandwichOrder{
public:
    virtual int GetCost() = 0;
    virtual std::string GetIngredient() = 0;
};

class BasicSandwich : public SandwichOrder{
public:
    int cost = 5;
    int GetCost() {
        return cost;
    }
    std::string GetIngredient() {
        return "Basic Bread and Meat";
    }
};

class DeluxeSandwich : public SandwichOrder{
public:
    int cost = 8;
    int GetCost() {
        return cost;
    }
    std::string GetIngredient() {
        return "Oraganic Bread, Organic Meat, Cheese, Veggie";
    }
};

Основният сандвич започва от $5 и съдържа само основен хляб и месо, докато Deluxe Sandwich започва от $8 и съдържа органичен хляб и месо, сирене и зеленчуци. Клиентът може да продължи да добавя гарнитури и подправки и клиентският код в крайна сметка ще върне цената и съставките на сандвича на клиента.

В момента клиентът (просто функция в този пример) може да произвежда само два вида сандвичи:

void ServeOrder(SandwichOrder* order) {
    std::cout << "Total Cost: " << order->GetCost() << std::endl;
    std::cout << "Ingredients: " << order->GetIngredient() << std::endl;
}

int main () {
    SandwichOrder* sandwich1 = new BasicSandwich;
    ServeOrder(sandwich1);
    SandwichOrder* sandwich2 = new DeluxeSandwich;
    ServeOrder(sandwich2);
    return 0;
}
// Output
// Total Cost: 5
// Ingredients: Basic Bread and Meat
// Total Cost: 8
// Ingredients: Oraganic Bread, Organic Meat, Cheese, Veggie

Сега нека създадем нашия BasicDecorator, който имплементира интерфейса SandwichOrder и също така съдържа екземпляр на обекта, който се опакова (wrappee).

class Decorator : public SandwichOrder {
public:
    SandwichOrder* order_;
    Decorator(SandwichOrder* order) : order_{order} {}
    int GetCost() { 
        return order_->GetCost();
    }
    std::string GetIngredient() {
        return order_->GetIngredient();
    }
};

Wrappee, указателят SandwichOrder, ще бъде инициализиран в конструкцията и Decorator заменя виртуалните функции GetCost() и GetIngredient(), като извиква извикванията на wrappee към тези методи. Изглежда, че в момента декораторът е просто празна обвивка, която препредава извикванията на метода на следващото ниво. Ако заменим извикването на клиента с обвивка, изходът трябва да остане същият като предишния

void ServeOrder(SandwichOrder* order) {
    std::cout << "Total Cost: " << order->GetCost() << std::endl;
    std::cout << "Ingredients: " << order->GetIngredient() << std::endl;
}

int main () {
    SandwichOrder* sandwich1 = new BasicSandwich;
    SandwichOrder* decorator1 = new Decorator(sandwich1);
    ServeOrder(decorator1);

    SandwichOrder* sandwich2 = new DeluxeSandwich;
    SandwichOrder* decorator2 = new Decorator(sandwich2);
    ServeOrder(decorator2);
    return 0;
}
// Output
// Total Cost: 5
// Ingredients: Basic Bread and Meat
// Total Cost: 8
// Ingredients: Oraganic Bread, Organic Meat, Cheese, Veggie

В този момент потенциалът на модела Decorator започва да се разкрива. Можем да увиваме слоеве и слоеве от различни декоратори около обекти, стига те да следват интерфейса на SandwichOrder.

Сега нека разширим класа Decorator, за да образуваме декоратор за добавяне на подправки. Можем да приемем, че добавянето на подправка е допълнителна такса от $1 на подправка и също така искаме да сме сигурни, че ще добавим това към списъка със съставките. Тъй като извикването на клиента към GetIngredient се нуждае от std::string за показване, ние искаме да върнем „мега“ std::string, който съдържа всички съставки в сандвича. Можем да обозначим всеки слой от добавения декоратор, за да върнем допълнителната подправка, която са добавили, и да го комбинираме със съставките, които се съдържат в опакования обект, за да получим агрегираната композиция. Започвате да звучите малко като рекурсия?

class CondimentDecorator : public Decorator {
public:
    std::string condiment_;
    int cost = 1;
    CondimentDecorator(std::string condiment, SandwichOrder* order) : 
        condiment_{condiment}, Decorator(order) {}
    int GetCost() {
        return cost + Decorator::GetCost();
    }

    std::string GetIngredient() {
        return condiment_ + " " + order_->GetIngredient();
    }

};

CondimentDecorator приема два аргумента по време на конструирането. Първият е типът подправка, която трябва да се добави, а вторият е опакованият сандвич, който се предава в конструктора на декоратора. След като това е внедрено, нека опитаме да добавим две различни подправки към нашия BasicSandwich.

int main () {
    SandwichOrder* sandwich1 = new BasicSandwich;
    SandwichOrder* decorated1 = new CondimentDecorator("mayo", sandwich1);
    SandwichOrder* decorated2 = new CondimentDecorator("mustard", decorated1);
    ServeOrder(decorated2);
    return 0;
}
// Output
// Total Cost: 7
// Ingredients: mustard mayo Basic Bread and Meat

Сега можем също да внедрим нашия клас SideDecorator, за да добавим различните страни към нашия SandwichOrder. Допълнителните страни са $2 на страна и отново искаме да върнем общите съставки и общата цена накрая.

class SideDecorator : public Decorator {
public:
    std::string side_;
    int cost = 2;
    
    SideDecorator(std::string side, SandwichOrder* order) : 
        side_{side}, Decorator(order) {}
    int GetCost() {
        return cost + Decorator::GetCost();
    }

    std::string GetIngredient() {
        return side_ + " " + order_->GetIngredient();
    }

};

Сега можем да експериментираме с различни комбинации от страни и подправки, добавени в различен ред, и да ги наблюдаваме рекурсивно разгръщане!

int main () {
    SandwichOrder* sandwich1 = new DeluxeSandwich;
    SandwichOrder* decorated1 = new CondimentDecorator("mayo", sandwich1);
    SandwichOrder* decorated2 = new SideDecorator("pickle", decorated1);
    SandwichOrder* decorated3 = new CondimentDecorator("mustard", decorated2);
    SandwichOrder* final_order = new SideDecorator("onion", decorated3);
    ServeOrder(final_order);
    return 0;
}
// Output
// Total Cost: 14
// Ingredients: onion mustard pickle mayo Oraganic Bread, Organic Meat, Cheese, Veggie

Това, което беше толкова очарователно за мен, е внедряването на рекурсия в шаблона на декоратора и гъвкавостта при добавяне на нови декоратори. Можете също така динамично да премахнете слой декоратор, но ще трябва да приложите метод в Decorator, който връща основния обвит обект.

SandwichOrder* unwrap = dynamic_cast<SandwichOrder*>(final_order)->getWrapped();

Някои други забавни декораторски проекти, които можете да опитате да пресъздадете, включват:

  • Форматиране на текст: започнете с прост пример за обработка на текст. Създайте клас Text, който представлява обикновен текст. След това внедрете декоратори като BoldDecorator, ItalicDecorator, UnderlineDecorator и добавете различни стилове за форматиране към текста.
  • Приготвяне на напитки: Подобно на примера със сандвич, но си представете различните декоратори, които можете да добавите към него.
  • ShapeDrawing: Да предположим, че създавате графично приложение. Започнете с основен клас Shape, който дефинира основното поведение на формата. Можете да реализирате различни форми като кръг, правоъгълник, триъгълник. Внедрете декоратори като ColorDecorator и PatternDecorator, които добавят визуални подобрения към формите, без да променят основното им поведение.