Das Decorator-Pattern eignet sich als Lösungsansatz für ganz unterschiedliche Problemstellungen. In diesem Anhang soll das Muster an einem konkreten Beispiel veranschaulicht werden, das nichts mit Ein- und Ausgabe zu tun hat.
Ein moderner Pizzadienst möchte seinen Betrieb mit Software unterstützen. Als Teil dieses Projekts sollen die verschiedenen Pizzas im Angebot durch Objekte modelliert werden. An diesem Beispiel wird das Decorator-Pattern plastisch umgesetzt.
Interface für alle Bausteine
Gemeinsame Eigenschaften aller Pizzas sind die Größe, der Preis sowie die Angaben, ob sie vegetarisch und ob sie scharf sind. Entsprechende Methoden werden in einem Interface festgelegt:
Als konkrete Bausteine dienen Pizzaböden, die „einfachsten” möglichen Pizzas. Die gemeinsamen Eigenschaften von Pizzaböden (Preis, scharf oder nicht) fasst die abstrakte Basisklasse[1]
Die Pizzaböden in diesem einfachen Beispiel sind sich so ähnlich, dass sogar ein Aufzählungstyp ausreichen würde.
Base zusammen. Alle Pizzaböden sind vegetarisch, Preis und Schärfe werden jeweils im Konstruktor festgelegt:
public abstract class Base implements Pizza { private final int price;
Die verschiedenen Auflagen werden als Dekoratoren implementiert. Die gemeinsame Eigenschaft aller Auflagen, die Verwaltung der darunterliegenden Pizza, wird in eine gemeinsame abstrakte Basisklasse Topping herausgezogen. Topping implementiert das Interface Pizza und enthält eine Objektvariable vom Typ Pizza, die die darunterliegende restliche Pizza ohne diese Auflage repräsentiert. Die Klasse ist abstrakt, obwohl sie keine abstrakten Methoden enthält. Damit können keine Instanzen erzeugt werden. Alle Methoden des Interface werden an die Objektvariable delegiert.
public abstract class Topping implements Pizza { private final Pizza below;
public Topping(Pizza below) { this.below = below; }
public boolean isVegetarian() { return below.isVegetarian(); }
public boolean isHot() { return below.isHot(); }
public int getPrice() { return below.getPrice(); } }
Von der abstrakten Basisklasse Topping können verschiedene konkrete Dekoratoren abgeleitet werden, die die ererbten Methoden nach Bedarf redefinieren
Jede Auflage Käse erhöht den Preis der gesamten Pizza um 1 Euro. Sie ändert nichts an den Eigenschaften „vegetarisch” und „scharf”. Die aus der Basisklasse ererbten Methoden isVegetarian und isHot rufen die entsprechenden Eigenschaften der restlichen Pizza, ohne diesen Käse, ab und geben das Ergebnis an den Aufrufer zurück. Beim Preis wird der Preis dieser Käseauflage (1 Euro) zum Preis der übrigen Pizza addiert und die Summe als Preis der ganzen Pizza (dieser Käse mit allem anderen) zurückgegeben.
public class Cheese extends Topping { public Cheese(Pizza below) { super(below); }
public int getPrice() { return 100 + super.getPrice(); } }
Cheese.java: Konkrete Dekoratorklasse: Käse als Pizzaauflage.
Salami als Auflage kostet 1,50 Euro. Eine Pizza mit Salami ist nicht vegetarisch, unabhängig vom darunterliegenden Rest der Pizza. Die Methode isVegetarian ruft deshalb die ererbte Methode nicht auf, sondern gibt sofort und ohne weitere Rückfrage das Ergebnis false zurück. Was immer unter dieser Salami liegt, die ganze Pizza kann wegen dieser Salami nicht vegetarisch sein.
public class Salami extends Topping { public Salami(Pizza below) { super(below); }
public int getPrice() { return 150 + super.getPrice(); }
public boolean isVegetarian() { return false; } }
Salami.java: Konkrete Dekoratorklasse: Salami als Pizzaauflage.
Gewürze sind kostenlos, machen eine Pizza aber scharf.
public class Chili extends Topping { public Chili(Pizza below) { super(below); }
public boolean isHot() { return true; } }
Chili.java: Konkrete Dekoratorklasse: Gewürze als Pizzaauflage.
Grundstruktur des Decorator-Pattern
Das folgende Diagramm zeigt die Beziehungen zwischen den Typen. Darin lässt sich die Grundstruktur des Decorator-Patterns erkennen (Seite ▶).
Aufbau einer Objektstruktur zur Laufzeit
Eine Anwendung kann aus diesen Typen zur Laufzeit beliebige Pizzas zusammenstellen und deren Eigenschaften abfragen. Das folgende Programm erwartet auf der Kommandozeile eine Reihe von Kürzeln mit jeweils den beiden kleinen Anfangsbuchstaben der gewünschten Bausteine. Als Erstes muss ein Pizzaboden genannt werden, daran anschließend die Auflagen.
public class PizzaMain { public static void main(String... args) { Pizza pizza = null; for(String arg: args) if(pizza == null) switch(arg) { case "Crunchy": pizza = new Crunchy(); break; case "Puffy": pizza = new Puffy(); break; case "Sicilian": pizza = new Sicilian(); break; } else switch(arg) { case "Cheese": pizza = new Cheese(pizza); break; case "Salami": pizza = new Salami(pizza); break; case "Chili": pizza = new Chili(pizza); break; } System.out.printf("Your pizza:%nprice: %d%nvegetarian: %b%nhot: %b%n", pizza.getPriceVerbose(""), pizza.isVegetarian(), pizza.isHot()); } }
PizzaMain.java: Programm, das eine Pizza nach Kommandozeilenargumenten zusammensetzt und ihre Eigenschaften ausgibt.
Beim Start des Programms können beliebige Pizzas zusammengestellt werden. Im folgenden Beispiel wird ein Pizzaboden der Art „Crunchy” mit zweimal Käse, einmal Salami und einmal Gewürzen belegt. Das Ergebnis ist eine Pizza mit einem Gesamtpreis von 6,50 Euro, die nicht vegetarisch, aber scharf ist:
$ java PizzaMain Crunchy Cheese Cheese Salami Chili Your pizza: price: 650 vegetarian: false hot: true
Rekursive Methodenaufrufe
Die folgende Ausgabe macht die Aufrufe der getPrice-Methoden im obigen Beispiel sichtbar. In spitzen Klammern ist jeweils die Objektreferenz angegeben, anhand derer gleiche und verschiedene Objekte identifiziert werden können.[2]
Der Wert des Codes ist ohne Bedeutung. Er wird von der toString-Methode geliefert, die in Object definiert ist. Es handelt sich um die hexadezimale Darstellung des Hashcodes, der auf der Speicheradresse des Objekts beruht.
Die Aufrufkette beginnt mit dem vordersten Dekorator, einem Chili-Objekt. Chili definiert keine eigene getPrice-Methode, sondern erbt sie von der Basisklasse. Das Topping-Objekt, dessen getPrice-Aufruf als Erstes protokolliert wird, delegiert den Aufruf an den nächsten Dekorator, das Salami-Objekt, das wiederum die Basisklassenmethode aufruft. Die Aufrufkette endet beim Base-Objekt, das ohne weitere Aufrufe den eigenen Preis, 300, zurückliefert. Bei der Rückkehr addieren die konkreten Auflagen ihren eigenen Preis zum Preis der restlichen Pizza und geben die Summe zurück:
Den Bausteinen des Decorator-Patterns sind die folgenden Typen der Pizza-Implementierung zugeordnet:
AbstractComponent
Pizza
ConcreteComponent
Base, Crunchy, Puffy, Sicilian
AbstractDecorator
Topping
ConcreteDecorator
Cheese, Salami, Chili
Grenzen des Musters
So elegante Lösungen das Decorator-Pattern für bestimmte Probleme auch liefert, so hat es doch auch Grenzen:
Stellen Sie sich vor, eine Pizza gilt erst dann als „scharf”, wenn zweimal Chili als Auflage verwendet wird. Diese unscheinbare Änderung macht einen Zähler für Chili nötig, der im Interface verankert werden muss. Der Zähler wird damit öffentlich sichtbar, obwohl ihn der Anwender nicht verlangt hat und auch nicht nutzen sollte.[3]
Man könnte eine entsprechende Methode vielleicht als protected definieren und damit die Sichtbarkeit einschränken. Allerdings ist das einerseits in einem Interface nicht möglich. Andererseits hat jede abgeleitete Klasse Zugriff auf protected-Elemente. Das gilt insbesondere für abgeleitete Klassen in beliebigen fremden Packages. Der Zugriffsschutz protected ist so gesehen kaum wirksamer als public.
Als zusätzliche Eigenschaft soll die Größe von Pizzas berücksichtigt werden. Auf den Gesamtpreis einer Pizza wirkt sich die Größe als Faktor aus, mit dem der Grundpreis am Ende multipliziert wird. Es stellt sich die Frage, an welcher Stelle diese Multiplikation stattfinden soll. Letztlich muss jede Komponente für sich die Multiplikation ausführen, weil keine einzelne Komponente die Gesamtkonstruktion kennt.