Menu
Coddy logo textTech

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.

challenge icon

Aufgabe

Einfach

Lassen 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 Pizza mit den rein virtuellen Methoden getDescription() (gibt einen String zurück) und getCost() (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 von 8.0 haben.

  • ToppingDecorator.h: Erstellen Sie die Decorator-Basisklasse und konkrete Belag-Decorator.

    Erstellen Sie eine Klasse ToppingDecorator, die von Pizza erbt und einen std::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 und 1.5 zu den Kosten hinzu
    • PepperoniTopping — fügt ", Pepperoni" zur Beschreibung und 2.0 zu den Kosten hinzu
    • MushroomTopping — fügt ", Mushrooms" zur Beschreibung und 1.0 zu 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 yes oder no):

    1. Käse hinzufügen?
    2. Salami (Pepperoni) hinzufügen?
    3. Pilze hinzufügen?

    Beginnen Sie mit einer PlainPizza und 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.5

Bei den Eingaben yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

Bei den Eingaben no, no, no:

Order: Pizza
Total: $8

Bei den Eingaben yes, yes, yes:

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

Beachten 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.7

Wann 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;
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung