Decorator-Muster
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 98 von 104.
Das Decorator-Pattern ermöglicht es Ihnen, Objekten dynamisch neue Verhaltensweisen hinzuzufügen, indem Sie diese in spezielle Objekte, sogenannte Decorator, einhüllen. Im Gegensatz zur Vererbung, die Verhalten zur Kompilierzeit hinzufügt, ermöglichen es Decorator, die Funktionalität zur Laufzeit zu erweitern, ohne die ursprüngliche Klasse zu ändern.
Das Muster funktioniert so, dass sowohl das ursprüngliche Objekt als auch die Decorator dieselbe Schnittstelle implementieren. Jeder Decorator hält eine Referenz auf eine Komponente und fügt sein eigenes Verhalten hinzu, bevor oder nachdem er an das umhüllte Objekt delegiert:
#include <iostream>
#include <memory>
// Komponenten-Schnittstelle
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};
// Konkrete Komponente
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};
// Basis-Decorator
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};
// Konkrete Decorator
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; }
};Dekoratoren können gestapelt werden – jeder umschließt den vorherigen und baut die Funktionalität Schicht für Schicht auf:
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();
// Ausgabe: Kaffee, Milch, Zucker: $2.7
}Verwenden Sie den Decorator, wenn Sie Objekten dynamisch Verantwortlichkeiten hinzufügen müssen, ohne andere Objekte zu beeinflussen, oder wenn die Erweiterung der Funktionalität durch Unterklassenbildung zu einer Explosion von Klassen führen würde.
Aufgabe
EinfachLassen Sie uns ein Pizza-Bestellsystem mit dem Decorator-Muster erstellen. Sie werden ein flexibles System entwickeln, bei dem Kunden mit einer Basis-Pizza beginnen und verschiedene Beläge hinzufügen können – jeder Belag umschließt die vorherige Bestellung und fügt seine eigenen Kosten und seine eigene Beschreibung hinzu. Dies ist ein perfekter Anwendungsfall für Decorator: Beläge können in jeder beliebigen Kombination gestapelt werden, ohne dass eine Explosion von Unterklassen entsteht.
Sie werden Ihren Code auf drei Dateien verteilen:
Pizza.h: Definieren Sie das Komponenten-Interface und die Basis-Pizza-Klasse.Erstellen Sie eine abstrakte Klasse
Pizzamit den rein virtuellen MethodengetDescription()(gibt einen String zurück) undgetCost()(gibt einen Double zurück), sowie einem virtuellen Destruktor.Erstellen Sie dann eine konkrete Klasse
PlainPizza, die die Basis-Pizza darstellt. Sie sollte"Pizza"als Beschreibung zurückgeben und einen Basispreis von8.0haben.ToppingDecorator.h: Erstellen Sie die Decorator-Basisklasse und konkrete Belag-Decorator.Erstellen Sie eine Klasse
ToppingDecorator, die vonPizzaerbt und einenstd::unique_ptr<Pizza>auf die umschlossene Komponente hält. Dies dient als Basis für alle Belag-Decorator.Implementieren Sie drei konkrete Belag-Decorator:
CheeseTopping— fügt", Cheese"zur Beschreibung und1.5zu den Kosten hinzuPepperoniTopping— fügt", Pepperoni"zur Beschreibung und2.0zu den Kosten hinzuMushroomTopping— fügt", Mushrooms"zur Beschreibung und1.0zu den Kosten hinzu
Jeder Decorator sollte an die umschlossene Pizza delegieren und dann seinen eigenen Beitrag hinzufügen.
main.cpp: Erstellen Sie eine individuelle Pizza-Bestellung basierend auf Benutzereingaben.Lesen Sie drei Eingaben ein (jeweils
yesoderno):- Käse hinzufügen?
- Salami (Pepperoni) hinzufügen?
- Pilze hinzufügen?
Beginnen Sie mit einer
PlainPizzaund umschließen Sie diese dann mit den entsprechenden Belag-Decoratoren basierend auf den Eingaben (in der Reihenfolge: Käse, Salami, Pilze). Geben Sie abschließend die Bestelldetails in diesem Format aus:Order: [description] Total: $[cost]
Zum Beispiel bei den Eingaben yes, yes, no:
Order: Pizza, Cheese, Pepperoni
Total: $11.5Bei den Eingaben yes, no, yes:
Order: Pizza, Cheese, Mushrooms
Total: $10.5Bei den Eingaben no, no, no:
Order: Pizza
Total: $8Bei den Eingaben yes, yes, yes:
Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5Beachten Sie, wie jeder Decorator den vorherigen umschließt und so Schicht für Schicht sowohl die Beschreibung als auch die Kosten aufbaut. Sie können problemlos neue Beläge hinzufügen, indem Sie neue Decorator-Klassen erstellen, ohne den bestehenden Pizza- oder Belag-Code zu ändern – das ist das Schöne am Decorator-Muster.
Spickzettel
Das Decorator-Muster ermöglicht es Ihnen, Objekten dynamisch neue Verhaltensweisen hinzuzufügen, indem Sie diese in spezielle Objekte, sogenannte Decorator, einhüllen. Im Gegensatz zur Vererbung, die Verhalten zur Kompilierzeit hinzufügt, ermöglichen Decorator es Ihnen, die Funktionalität zur Laufzeit zu erweitern, ohne die ursprüngliche Klasse zu ändern.
Das Muster funktioniert so, dass sowohl das ursprüngliche Objekt als auch die Decorator dieselbe Schnittstelle implementieren. Jeder Decorator hält eine Referenz auf eine Komponente und fügt sein eigenes Verhalten hinzu, bevor oder nachdem er die Anfrage an das eingehüllte Objekt delegiert.
Struktur
Component-Schnittstelle: Definiert die gemeinsame Schnittstelle sowohl für konkrete Komponenten als auch für Decorator.
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};Konkrete Komponente: Das Basisobjekt, das dekoriert werden kann.
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};Basis-Decorator: Hält eine Referenz auf eine Komponente und implementiert dieselbe Schnittstelle.
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};Konkrete Decorator: Fügen spezifische Verhaltensweisen hinzu, indem sie an das eingehüllte Objekt delegieren und ihre eigene Funktionalität ergänzen.
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; }
};Stapeln von Decorators
Decorator können gestapelt werden – jeder hüllt den vorherigen ein und baut die Funktionalität Schicht für Schicht auf:
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();
// Output: Coffee, Milk, Sugar: $2.7Wann man es verwendet
Verwenden Sie den Decorator, wenn Sie Objekten dynamisch Verantwortlichkeiten hinzufügen müssen, ohne andere Objekte zu beeinflussen, oder wenn die Erweiterung der Funktionalität durch Unterklassenbildung zu einer Explosion von Klassen führen würde.
Probier es selbst
#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"
using namespace std;
int main() {
// Lies drei Eingaben (yes oder no)
string addCheese, addPepperoni, addMushrooms;
cin >> addCheese;
cin >> addPepperoni;
cin >> addMushrooms;
// TODO: Beginne mit einer PlainPizza unter Verwendung von std::unique_ptr
// TODO: Wenn addCheese "yes" ist, umhülle die Pizza mit CheeseTopping
// TODO: Wenn addPepperoni "yes" ist, umhülle die Pizza mit PepperoniTopping
// TODO: Wenn addMushrooms "yes" ist, umhülle die Pizza mit MushroomTopping
// TODO: Gib die Bestellung in folgendem Format aus:
// Order: [description]
// Total: $[cost]
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins via CRTPPimpl-IdiomType ErasureEnum-Klassen & starke TypisierungException-Handling in OOPEigene Exception-Hierarchien14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterRAII als Muster3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container