Хотя дизайн декоратора не считается одним из наиболее важных шаблонов проектирования, которые нужно освоить программисту, мне определенно было очень весело изучать его из-за своеобразного эффекта, который он создает при реализации. Эта статья будет немного отличаться от других статей этой серии, таких как Фабричный метод, Строитель, Шаблон проектирования адаптер, где я пытаюсь строго определить более полную реализацию этих, казалось бы, простых шаблонов проектирования. Скорее я просто поделюсь своими знаниями, полученными в результате изучения паттерна Декоратор в книге Гуру рефакторинга на примере C++.

Дизайн-декоратор — это творческий шаблон, позволяющий улучшать интерфейсы. Усовершенствованный объект образует структуру, подобную стеку, со слоями и слоями дополнительных атрибутов, добавленных в соответствии с принципами «Последним поступил — первым отправлен» (LIFO). При использовании декорированного объекта каждый слой и каждый добавленный атрибут рекурсивно расширяются, образуя поведение, напоминающее русскую матрешку. Я действительно думаю, что идея улучшения и расширения объекта путем наложения декоратора поверх него увлекательна, но по сути фундаментальные характеристики обернутого объекта не изменились.

Предположим, мы строим магазин сэндвичей, где пользователь имеет возможность выбрать основу сэндвича, а затем добавить к нему гарниры и приправы. В основе сэндвича есть базовая версия, в которой используются обычный хлеб и мясо, а в версии Deluxe используются органический хлеб и мясо, а также сыр и салат. После того, как клиент выберет основу и выберет дополнительные гарниры и приправы, сэндвич-магазин вернет общую стоимость и различные ингредиенты, из которых был приготовлен сэндвич.

Отправной точкой для разработки этого класса было бы создание базового класса Sandwich и расширение его до расширенных и базовых дочерних классов Sandwich. Очевидно, нам не хотелось бы продолжать выводить больше подклассов для различных сторон и приправ, иначе количество подклассов будет продолжать расти. Именно здесь вступает в силу принцип предпочитать композицию наследованию.

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

Основная идея декоратора заключается в том, что мы можем использовать этот класс для улучшения сэндвич-классов. У нас могут быть CondimentDecorator и SideDecorator, которые будут использоваться для обертывания сэндвич-классов для улучшения и расширения его функциональности. На следующем графике сравниваются два подхода.

Прежде чем перейти к конкретной реализации, давайте удостоверимся, что мы понимаем определения и UML шаблона Декоратора. По мнению Гуру рефакторинга:

Декоратор — это шаблон структурного проектирования, который позволяет прикреплять к объектам новые варианты поведения, помещая эти объекты внутри специальных объектов-оболочек, содержащих эти варианты поведения.

Идея состоит в том, что новое поведение украшается путем включения в него старых объектов. Кроме того, для клиентского кода вызов декорированного или исходного объекта не должен иметь значения.

Интерфейс компонента — это то, с чем взаимодействует клиент, а конкретные компоненты реализуют интерфейс. В нашем примере с сэндвич-магазином сэндвич является компонентом, а конкретные компоненты — это базовые и роскошные сэндвичи. Это строительные блоки, с которых мы начнем.

Базовый декоратор также реализует интерфейс компонентов. Это может показаться странным для визуализации, потому что зачем CondimentDecorator реализовывать Sandwich. Все это обретет смысл через секунду. Наконец, ConcreteDecorators — это специальные декораторы, которые мы хотим использовать для украшения конкретного компонента, например CondimentDecorator или SideDecorator.

Вы заметите, что BaseDecorator не только реализует интерфейс компонента, но также имеет экземпляр компонента (агрегация). Экземпляр Компонента — это то, что Декоратор упаковывает внутрь.

Сначала мы реализуем интерфейс компонентов и конкретные компоненты в нашем магазине сэндвичей:

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 долларов и содержит только основной хлеб и мясо, а сэндвич Делюкс начинается с 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();
    }
};

Оболочка, указатель SandwichOrder, будет инициализирована при создании, а Декоратор переопределяет виртуальные функции GetCost() и GetIngredient(), вызывая вызовы оболочки к этим методам. Похоже, что сейчас Decorator — это просто пустая оболочка, которая передает вызовы методов на следующий уровень. Если мы заменим клиентский вызов оберткой, вывод останется таким же, как и предыдущий.

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

На этом этапе начинает раскрываться потенциал шаблона «Декоратор». Мы можем обертывать объекты слоями и слоями различных декораторов, если они следуют интерфейсу 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 во время построения принимает два аргумента. Первый — это тип добавляемой приправы, а второй — завернутый сэндвич, который передается в конструктор Decorator. После этого давайте попробуем добавить в наш 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, которые добавляют визуальные улучшения к фигурам, не изменяя их основное поведение.