Menu
Coddy logo textTech

Паттерн Декоратор

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 98 из 104.

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

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

#include <iostream>
#include <memory>

// Интерфейс компонента
class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

// Конкретный компонент
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

// Базовый декоратор
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

// Конкретные декораторы
class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Sugar";
    }
    double getCost() const override { return coffee->getCost() + 0.2; }
};

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

int main() {
    std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
    order = std::make_unique<MilkDecorator>(std::move(order));
    order = std::make_unique<SugarDecorator>(std::move(order));
    
    std::cout << order->getDescription() << ": $" << order->getCost();
    // Вывод: Coffee, Milk, Sugar: $2.7
}

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

challenge icon

Задание

Легко

Давайте создадим Систему заказа пиццы, используя паттерн Декоратор. Вы создадите гибкую систему, в которой клиенты могут начать с базовой пиццы и добавлять различные топпинги — каждый топпинг оборачивает предыдущий заказ и добавляет свою стоимость и описание. Это идеальный вариант использования декораторов: топпинги можно комбинировать в любом порядке без создания огромного количества подклассов.

Вы распределите свой код по трем файлам:

  • Pizza.h: Определите интерфейс компонента и базовый класс пиццы.

    Создайте абстрактный класс Pizza с чисто виртуальными методами getDescription() (возвращает строку) и getCost() (возвращает double), а также виртуальным деструктором.

    Затем создайте конкретный класс PlainPizza, который представляет базовую пиццу. Он должен возвращать "Pizza" в качестве описания и иметь базовую стоимость 8.0.

  • ToppingDecorator.h: Создайте базовый класс декоратора и конкретные декораторы топпингов.

    Создайте класс ToppingDecorator, который наследуется от Pizza и содержит std::unique_ptr<Pizza> для обернутого компонента. Это послужит основой для всех декораторов топпингов.

    Реализуйте три конкретных декоратора топпингов:

    • CheeseTopping — добавляет ", Cheese" к описанию и 1.5 к стоимости
    • PepperoniTopping — добавляет ", Pepperoni" к описанию и 2.0 к стоимости
    • MushroomTopping — добавляет ", Mushrooms" к описанию и 1.0 к стоимости

    Каждый декоратор должен делегировать выполнение обернутой пицце, а затем добавлять свой собственный вклад.

  • main.cpp: Создайте индивидуальный заказ пиццы на основе ввода пользователя.

    Прочитайте три входных значения (каждое будет yes или no):

    1. Добавить сыр?
    2. Добавить пепперони?
    3. Добавить грибы?

    Начните с PlainPizza, затем оберните ее соответствующими декораторами топпингов на основе входных данных (в порядке: сыр, пепперони, грибы). Наконец, выведите детали заказа в следующем формате:

    Order: [description]
    Total: $[cost]

Например, при входных данных yes, yes, no:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

При входных данных yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

При входных данных no, no, no:

Order: Pizza
Total: $8

При входных данных yes, yes, yes:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

Обратите внимание, как каждый декоратор оборачивает предыдущий, слой за слоем формируя описание и стоимость. Вы можете легко добавлять новые топпинги, создавая новые классы декораторов без изменения существующего кода пиццы или топпингов — в этом и заключается прелесть паттерна Декоратор.

Шпаргалка

Паттерн Декоратор позволяет динамически добавлять объектам новые обязанности, оборачивая их в специальные объекты-обертки, называемые декораторами. В отличие от наследования, которое добавляет поведение на этапе компиляции, декораторы позволяют расширять функциональность во время выполнения, не изменяя исходный класс.

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

Структура

Интерфейс компонента: Определяет общий интерфейс как для конкретных компонентов, так и для декораторов.

class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

Конкретный компонент: Базовый объект, который может быть декорирован.

class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

Базовый декоратор: Хранит ссылку на компонент и реализует тот же интерфейс.

class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

Конкретные декораторы: Добавляют специфическое поведение, делегируя выполнение обернутому объекту и добавляя свою собственную функциональность.

class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

Наслоение декораторов

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

std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));

std::cout << order->getDescription() << ": $" << order->getCost();
// Вывод: Coffee, Milk, Sugar: $2.7

Когда использовать

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

Попробуйте сами

#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"

using namespace std;

int main() {
    // Считать три входных значения (yes или no)
    string addCheese, addPepperoni, addMushrooms;
    cin >> addCheese;
    cin >> addPepperoni;
    cin >> addMushrooms;
    
    // TODO: Начните с PlainPizza, используя std::unique_ptr
    
    // TODO: Если addCheese равно "yes", оберните пиццу в CheeseTopping
    
    // TODO: Если addPepperoni равно "yes", оберните пиццу в PepperoniTopping
    
    // TODO: Если addMushrooms равно "yes", оберните пиццу в MushroomTopping
    
    // TODO: Выведите заказ в формате:
    // Order: [description]
    // Total: $[cost]
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование