Паттерн Декоратор
Часть раздела Объектно-ориентированное программирование путешествия по 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
}Используйте паттерн Декоратор, когда вам нужно динамически добавлять объектам новые обязанности, не затрагивая другие объекты, или когда расширение функциональности с помощью подклассов приводит к разрастанию иерархии классов.
Задание
ЛегкоДавайте создадим Систему заказа пиццы, используя паттерн Декоратор. Вы создадите гибкую систему, в которой клиенты могут начать с базовой пиццы и добавлять различные топпинги — каждый топпинг оборачивает предыдущий заказ и добавляет свою стоимость и описание. Это идеальный вариант использования декораторов: топпинги можно комбинировать в любом порядке без создания огромного количества подклассов.
Вы распределите свой код по трем файлам:
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):- Добавить сыр?
- Добавить пепперони?
- Добавить грибы?
Начните с
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;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер