Wzorzec dekoratora
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 98 z 104.
Wzorzec Dekoratora pozwala dynamicznie dodawać nowe zachowania do obiektów, opakowując je w specjalne obiekty nazywane dekoratorami. W przeciwieństwie do dziedziczenia, które dodaje zachowanie podczas kompilacji, dekoratory pozwalają rozszerzać funkcjonalność w czasie działania bez modyfikowania oryginalnej klasy.
Wzorzec działa dzięki temu, że zarówno oryginalny obiekt, jak i dekoratory implementują ten sam interfejs. Każdy dekorator przechowuje referencję do komponentu i dodaje własne zachowanie przed przekazaniem wywołania do opakowanego obiektu lub po nim:
#include <iostream>
#include <memory>
// Interfejs komponentu
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};
// Konkretny komponent
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};
// Dekorator bazowy
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};
// Konkretne dekoratory
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; }
};Dekoratory można nakładać jeden na drugi — każdy opakowuje poprzedni, dodając kolejne warstwy funkcjonalności:
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();
// Wynik: Coffee, Milk, Sugar: $2.7
}Użyj wzorca Dekorator, gdy potrzebujesz dynamicznie dodawać obiektom obowiązki bez wpływu na inne obiekty lub gdy rozszerzanie funkcjonalności przez dziedziczenie prowadziłoby do eksplozji liczby klas.
Wyzwanie
ŁatwyZbudujmy System zamawiania pizzy przy użyciu wzorca Dekorator. Stworzysz elastyczny system, w którym klienci mogą zacząć od podstawowej pizzy i dodać różne dodatki: każdy dodatek opakowuje poprzednie zamówienie i dodaje własny koszt oraz opis. To idealne zastosowanie dekoratorów: dodatki można łączyć w dowolnej konfiguracji bez tworzenia ogromnej liczby podklas.
Rozmieścisz kod w trzech plikach:
Pizza.h: Zdefiniuj interfejs komponentu i klasę podstawowej pizzy.Utwórz abstrakcyjną klasę
Pizzaz czysto wirtualnymi metodamigetDescription()(zwraca ciąg znaków) igetCost()(zwraca liczbę typu double), a także wirtualnym destruktorem.Następnie utwórz konkretną klasę
PlainPizza, która reprezentuje podstawową pizzę. Powinna zwracać"Pizza"jako opis i mieć koszt podstawowy równy8.0.ToppingDecorator.h: Utwórz bazową klasę dekoratora i konkretne dekoratory dodatków.Utwórz klasę
ToppingDecorator, która dziedziczy poPizzai przechowujestd::unique_ptr<Pizza>do opakowywanego komponentu. Będzie ona klasą bazową dla wszystkich dekoratorów dodatków.Zaimplementuj trzy konkretne dekoratory dodatków:
CheeseTopping: dodaje", Cheese"do opisu i1.5do kosztuPepperoniTopping: dodaje", Pepperoni"do opisu i2.0do kosztuMushroomTopping: dodaje", Mushrooms"do opisu i1.0do kosztu
Każdy dekorator powinien delegować wywołanie do opakowanej pizzy, a następnie dodawać własny wkład.
main.cpp: Utwórz spersonalizowane zamówienie pizzy na podstawie danych wprowadzonych przez użytkownika.Odczytaj trzy dane wejściowe (każda z nich będzie miała wartość
yeslubno):- Dodać ser?
- Dodać pepperoni?
- Dodać pieczarki?
Zacznij od
PlainPizza, a następnie opakuj ją odpowiednimi dekoratorami dodatków na podstawie wprowadzonych danych (w kolejności: ser, pepperoni, pieczarki). Na koniec wyświetl szczegóły zamówienia w następującym formacie:Order: [description] Total: $[cost]
Na przykład, dla danych wejściowych yes, yes, no:
Order: Pizza, Cheese, Pepperoni
Total: $11.5Dla danych wejściowych yes, no, yes:
Order: Pizza, Cheese, Mushrooms
Total: $10.5Dla danych wejściowych no, no, no:
Order: Pizza
Total: $8Dla danych wejściowych yes, yes, yes:
Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5Zauważ, jak każdy dekorator opakowuje poprzedni, stopniowo rozbudowując opis i koszt. Możesz łatwo dodawać nowe dodatki, tworząc nowe klasy dekoratorów, bez modyfikowania istniejącego kodu pizzy ani dodatków: na tym polega piękno wzorca Dekorator.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"
using namespace std;
int main() {
// Wczytaj trzy odpowiedzi (tak lub nie)
string addCheese, addPepperoni, addMushrooms;
cin >> addCheese;
cin >> addPepperoni;
cin >> addMushrooms;
// TODO: Zacznij od PlainPizza, używając std::unique_ptr
// TODO: Jeśli addCheese ma wartość "yes", opakuj pizzę w CheeseTopping
// TODO: Jeśli addPepperoni ma wartość "yes", opakuj pizzę w PepperoniTopping
// TODO: Jeśli addMushrooms ma wartość "yes", opakuj pizzę w MushroomTopping
// TODO: Wypisz zamówienie w formacie:
// Order: [description]
// Total: $[cost]
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów11Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieMixiny z użyciem CRTPIdiom PimplWymazywanie typówKlasy enum i silne typowanieObsługa wyjątków w OOPWłasne hierarchie wyjątków14Wzorce projektowe, część 2
Wzorzec poleceniaWzorzec adapteraWzorzec dekoratoraWzorzec metody szablonowejWzorzec stanuWzorzec kompozytuRAII jako wzorzec3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online