Decorator
Kurz gesagt: Decorator umhüllt ein Objekt mit einem anderen Objekt derselben Schnittstelle und fügt Verhalten hinzu, ohne die Klasse zu ändern und ohne eine Klasse für jede Kombination.
Problem
Die Karte, die mit jedem Extra wächst
Das Café verkauft Espresso für 2 €. Der Kunde kann Milch (+0,40 €), Sirup (+0,60 €) und einen zusätzlichen Shot (+0,80 €) dazunehmen. Jemand schreibt für jede Kombination eine Klasse:
class Espresso { public double cost() { return 2.00; }
public String description() { return "Espresso"; }}
class EspressoWithMilk extends Espresso { @Override public double cost() { return super.cost() + 0.40; }
@Override public String description() { return super.description() + ", Milch"; }}
class EspressoWithMilkAndSyrup extends EspressoWithMilk { @Override public double cost() { return super.cost() + 0.60; }
@Override public String description() { return super.description() + ", Sirup"; }}Espresso drink = new Espresso();System.out.printf("%s: %.2f €%n", drink.description(), drink.cost());drink = new EspressoWithMilk();System.out.printf("%s: %.2f €%n", drink.description(), drink.cost());drink = new EspressoWithMilkAndSyrup();System.out.printf("%s: %.2f €%n", drink.description(), drink.cost());Espresso: 2.00 €Espresso, Milch: 2.40 €Espresso, Milch, Sirup: 3.00 €Für die drei Getränke auf der Karte funktioniert das. Aber drei Extras ergeben acht Kombinationen, und mit Cappuccino und Tee sind es 24 Klassen. Ein weiteres Extra verdoppelt die Zahl.
EspressoWithSyrup (ohne Milch) kann den Sirup nicht von EspressoWithMilkAndSyrup erben, weil eine Klasse nur eine Basisklasse hat. Der Sirup-Code wird kopiert, und wenn der Sirup teurer wird, ändert er sich an mehreren Stellen.Und der Kunde möchte zweimal Sirup. Eine solche Klasse gibt es nicht. Eine Klasse für einen, zwei und drei Sirups ist keine Lösung.
Die Idee
Decorator: eine Hülle mit derselben Form
Ein Extra ist keine Art von Getränk. Es ist etwas, das ein Getränk umhüllt: Es nimmt das Getränk und fügt seinen eigenen Teil hinzu.
Deshalb haben alle eine gemeinsame Form: das InterfaceDrink (IDrink in C#) mit Preis und Beschreibung. Der Espresso implementiert es und kennt seinen eigenen Preis. Auch Milk implementiert es, hält aber in einem Feld ein anderes Getränk, das ein Drink ist. Wird es nach dem Preis gefragt, fragt es das innere Getränk und addiert 0,40 €.Das ist das Decorator-Muster. Es hat drei Rollen:
- die gemeinsame Form: das Interface, das die Getränke und die Hüllen gemeinsam haben;
- die Basis: der Espresso, der nichts umhüllt;
- die Decorators: Milch, Sirup, der zusätzliche Shot. Jeder hält ein inneres Getränk, ruft darauf dieselbe Methode auf und fügt seinen eigenen Teil hinzu.
Weil die Hülle dieselbe Form hat wie das Getränk, kann eine Hülle eine Hülle umhüllen. Doppelter Sirup sind zwei Schichten Sirup. Der Code, der nach dem Preis fragt, weiß nicht, wie viele Schichten es gibt.
Sieh es dir an
Schicht für Schicht
Die gemeinsame Form:
interface Drink { double cost(); String description();}Der Espresso und zwei Extras. Jedes Extra bekommt das innere Getränk im Konstruktor:
class Espresso implements Drink { @Override public double cost() { return 2.00; }
@Override public String description() { return "Espresso"; }}
class Milk implements Drink { private final Drink inner;
public Milk(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() + 0.40; }
@Override public String description() { return inner.description() + ", Milch"; }}
class Syrup implements Drink { private final Drink inner;
public Syrup(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() + 0.60; }
@Override public String description() { return inner.description() + ", Sirup"; }}Die Bestellung wird von innen nach außen zusammengesetzt: zuerst der Espresso, dann umhüllt jede Schicht das, was es bisher gibt.
Drink drink = new Espresso();drink = new Milk(drink);drink = new Syrup(drink);drink = new Syrup(drink);System.out.printf("%s: %.2f €%n", drink.description(), drink.cost());Espresso, Milch, Sirup, Sirup: 3.60 €Drink drink = new Espresso();drink = new Milk(drink);drink = new Syrup(drink);drink = new Syrup(drink);System.out.printf("%s: %.2f €%n", drink.description(), drink.cost());Schritt für Schritt, in Worten
- Der Espresso ist die Basis. Er umhüllt nichts und kennt seinen Preis: 2.00.
- Milch umhüllt den Espresso und hält ihn im Feld inner. drink zeigt jetzt auf die Milch, die ebenfalls ein Drink ist.
- Der Sirup umhüllt die Milch. Die Milch und der Espresso darin werden nicht angefasst.
- Noch einmal Sirup: eine zweite Schicht derselben Klasse. Das ist der doppelte Sirup.
- cost() geht an die äußerste Schicht. Sie addiert noch nichts: Zuerst ruft sie inner.cost() auf, und so macht es jede Schicht auf dem Weg nach innen.
- Der Espresso antwortet 2.00. Auf dem Weg nach außen addiert jede Schicht ihren Teil zu dem, was von innen kommt.
- Aus der äußersten Schicht kommt 3.60 heraus. description() nimmt denselben Weg und sammelt die Wörter.
Der Aufruf von cost() geht Schicht für Schicht nach innen bis zum Espresso, und die Summe wächst auf dem Weg nach außen. Keine der drei Klassen weiß etwas von den anderen.
Sieh es dir an
Eine neue Schicht, ohne die alten anzufassen
Der zusätzliche Shot und der Happy-Hour-Rabatt sind zwei neue Klassen. Der Rabatt addiert nicht, er multipliziert alles, was innen ist, mit 0,9:
class ExtraShot implements Drink { private final Drink inner;
public ExtraShot(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() + 0.80; }
@Override public String description() { return inner.description() + ", Extra-Shot"; }}
class Discount implements Drink { private final Drink inner;
public Discount(Drink inner) { this.inner = inner; }
@Override public double cost() { return inner.cost() * 0.9; }
@Override public String description() { return inner.description() + ", 10 % Rabatt"; }}Drink order = new Discount(new ExtraShot(new Milk(new Espresso())));System.out.printf("%s: %.2f €%n", order.description(), order.cost());Espresso, Milch, Extra-Shot, 10 % Rabatt: 2.88 €Espresso, Milk und Syrup haben sich nicht geändert. Die neue Schicht legt sich um jedes Getränk, auch um eines, das schon umhüllt ist.
Du liest new Discount(new ExtraShot(new Milk(new Espresso()))) von innen nach außen: ein Espresso, umhüllt von Milch, umhüllt von einem Shot, umhüllt von einem Rabatt. Die Reihenfolge zählt: Der Rabatt gilt nur für das, was innen ist.
Eine Hülle um etwas, das kein Getränk ist, lässt sich nicht kompilieren:
Drink drink = new Milk("Espresso");error: incompatible types: String cannot be converted to DrinkDer Konstruktor will ein Drink, und der Compiler stoppt vor der ersten Bestellung.
Sieh es dir an
Eine Hülle um eine Methode
Dieselbe Idee funktioniert auch außerhalb des Kaffees. Der Bestellservice läuft, und jetzt soll jeder Aufruf sichtbar sein: was gefragt wurde und was zurückkam. Der Code des Services soll nicht angefasst werden: Er ist getestet, und die Logs sind vorübergehend.
Der Service steht hinter einem Interface:
interface OrderService { int place(String drink, int quantity);}
class CafeOrderService implements OrderService { private int next = 1001;
@Override public int place(String drink, int quantity) { System.out.println("Zubereitung: " + quantity + " × " + drink); return next++; }}Die Hülle hat dasselbe Interface, schreibt das Log und ruft den echten Service auf:
class LoggingOrderService implements OrderService { private final OrderService inner;
public LoggingOrderService(OrderService inner) { this.inner = inner; }
@Override public int place(String drink, int quantity) { System.out.println("[Log] place(" + drink + ", " + quantity + ")"); int number = inner.place(drink, quantity); System.out.println("[Log] place lieferte " + number); return number; }}OrderService orders = new LoggingOrderService(new CafeOrderService());orders.place("Cappuccino", 2);orders.place("Espresso", 1);[Log] place(Cappuccino, 2)Zubereitung: 2 × Cappuccino[Log] place lieferte 1001[Log] place(Espresso, 1)Zubereitung: 1 × Espresso[Log] place lieferte 1002Der Code, der Bestellungen annimmt, bekommt einen OrderService und weiß nicht, dass er umhüllt ist. Du entfernst die Logs, indem du den Service ohne die Hülle übergibst.
Dasselbe gilt für Zeitmessung, einen Cache oder einen neuen Versuch nach einem Fehler: Jedes davon ist eine eigene Schicht.
Genauer, für Neugierige
Java hat Decorators in seiner Standardbibliothek: new BufferedReader(new InputStreamReader(System.in)) ist ein Eingabestrom, umhüllt von einem Strom, der zeilenweise liest. Spring legt Hüllen um deine Klassen, um Transaktionen oder einen Cache hinzuzufügen (@Transactional, @Cacheable).
Probier es aus
Was kommt heraus?
Der Rabatt und der Sirup sind in beiden Fällen dieselben, nur in anderer Reihenfolge. Was gibt das Programm aus?
Drink a = new Discount(new Syrup(new Espresso()));Drink b = new Syrup(new Discount(new Espresso()));System.out.printf("%.2f €%n", a.cost());System.out.printf("%.2f €%n", b.cost());Probier es aus
Finde den Fehler
Jemand schreibt den Sirup um:
class Syrup implements Drink { private final Drink inner;
public Syrup(Drink inner) { this.inner = inner; }
@Override public double cost() { return 0.60; }
@Override public String description() { return inner.description() + ", Sirup"; }}Drink drink = new Syrup(new Milk(new Espresso()));System.out.printf("%s: %.2f €%n", drink.description(), drink.cost());Espresso, Milch, Sirup: 0.60 €Die Beschreibung stimmt, und die Summe ist 0,60 €. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Der Sirup gibt nur seinen eigenen Preis zurück und fragt das innere Getränk nicht. Alles darunter (der Espresso und die Milch) verschwindet ohne Fehlermeldung aus der Summe. Die Beschreibung stimmt, weil description() das innere Getränk durchaus fragt, und genau deshalb übersieht man den Fehler leicht. Ein Decorator muss dieselbe Methode auf dem Objekt aufrufen, das er umhüllt: return inner.cost() + 0.60;. Dann ist die Summe 3 €.
Die Idee
Wann du es nicht brauchst
Decorator hat seinen Preis: eine Klasse für jede Schicht, und die Reihenfolge ist ein Objekt in einem Objekt in einem Objekt. Es lohnt sich nicht, wenn:
- jedes Extra nur einen Preis und einen Namen hinzufügt. Dann sind die Extras Daten: eine Liste von Paaren „Name, Preis“, und die Summe ist der Preis der Basis plus die Summe der Liste. Ein neuer Sirup ist eine neue Zeile in der Karte, keine neue Klasse;
- es eine einzige Schicht gibt, die nie entfernt wird. Ihr Code kann in der Klasse selbst stehen;
- das umhüllte Interface zwanzig Methoden hat. Jede Hülle muss alle zwanzig weiterreichen, auch wenn sie nur eine ändert.
Achte auch auf tiefe Stapel: Sechs Schichten sind sechs Zeilen in einem Stacktrace, und ihre Reihenfolge ist nur dort sichtbar, wo sie zusammengesetzt werden.
Es lohnt sich, wenn die Schichten Verschiedenes tun (einen Rabatt, Logging, einen Cache, einen neuen Versuch), für verschiedene Fälle unterschiedlich zusammengesetzt werden und du das, was du umhüllst, nicht anfassen willst.
Übung
Cappuccino mit Sahne
Löse die Aufgabe in deinem Editor.
Cappuccino mit Sahne. Das Café nimmt einen Cappuccino für 2,80 € und Sahne für 0,50 € dazu. Schreibe die Klassen Cappuccino und WhippedCream und bestelle einen Cappuccino mit zweimal Sahne. Die alten Klassen werden nicht angefasst.
Cappuccino, Sahne, Sahne: 3.80 €Weitere Aufgaben
- Wie lange es dauert. Schreibe einen
TimingOrderService, der die Zeit jedesplacemisst und ausgibt, wie viele Millisekunden es gedauert hat. Nutze es zusammen mit dem Logging. Welche Schicht liegt außen, und warum ist das wichtig? - Ein großer Becher. Ein großer Becher kostet 30 % mehr. Füge eine Schicht
Largehinzu. Lege sie einmal um den Espresso und einmal um die ganze Bestellung mit Milch und Sirup. Welche Variante ist für den Kunden fair? - Ein Zähler. Eine Hülle um den Service zählt, wie viele Bestellungen durchgegangen sind, und gibt die Zahl am Ende aus. Überlege, wo der Zähler lebt: in der Hülle oder im Service.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces · Exceptions · Lambdas · Entwurfsmuster
- Geht weiter in: Der Shop: welches Muster wohin · Caching
- Aufgaben zum Thema: Entwurfsmuster