Vanilleeis mit Sahne und Schokosoße? Kein Problem mit dem Decorator Pattern

Für Sie interessant:

 

Design Patterns in Java

In dem Seminar Design Patterns in Java lernen Sie komplexe Problemstellungen mit Hilfe wiederverwendbarer Softwarekomponenten zu lösen.

Wer kennt das nicht? Gerade hat man eine nette kleine Eisdiele eröffnet und alles scheint super zu laufen. Das Sortiment umfasst die Eissorten Vanille, Schokolade, Zitrone, leckere Erdbeere und Mokka. Pistazie wird auch auf mehrfache Nachfrage nicht im Sortiment aufgenommen.

Als moderne Eisdiele haben Sie selbstverständlich auch eine Software, die Sie beim Verkauf unterstützt. Das aktuelle Design besteht aus einer abstrakten Klasse Eis und mehreren Unterklassen (eine Klasse pro Eis-Sorte).

Abstrakte Klassen Eis mit mehreren Unterklassen

Die Kunden sind trotz des limitierten Sortiments zufrieden. Jedoch gibt es immer wieder Nachfragen, ob es nicht möglich sei ein Eis mit Sahne oder Schokosoße (denkbar sind natürlich noch viele andere Toppings) zu bekommen.

Praktisch ist das natürlich möglich, aber die Umsetzung in der Software hat Sie ziemlich viel Arbeit gekostet. Heraus kam folgendes Design:

Abstrakte Klasse Eis mit Toppings

Gerade wollten Sie sich nach einem Tag harter Programmierarbeit zurücklehnen, da kommt schon wieder ein Kunde. „Ein Zitroneneis mit Sahne und Schokosoße bitte!“. Sahne und Schokosoße? Da müssen Sie ja noch mehr Klassen schreiben… Gar nicht auszudenken, was passiert, wenn Sie jetzt auch noch Erdbeersoße oder Streusel als Toppings einführen wollen… Gibt es da keine bessere Lösung?

Dann wollen wir doch mal nachdenken: Es wäre sinnvoll, wenn man Eissorten und Toppings irgendwie einzeln behandeln könnte. Trotzdem wäre es praktisch, wenn ich für eine Bestellung nur ein Objekt bekomme. Es sollte also möglich sein, die Komponenten ineinander zu „verpacken“.

Das Matrjoschka-Prinzip

Bild: Flickr.com/ Cha già José

Bild: Flickr.com/ Cha già José

Dieses Matrjoschka-Prinzip“ lässt sich dadurch erreichen, dass eine Klasse ein Objekt derselben Klasse speichert.

Bestimmte Komponenten sind die Basis für diese verschachtelten Objekte und enthalten selbst keine weiteren Objekte mehr. In unserem Fall wäre dies das Eis. Bildlich kann man sich das so vorstellen, dass das Eis mit Sahne bedeckt wird. Die Sahne wiederum wird mit Schokosoße überdeckt. Zu guter Letzt kommt auf die Schokosoße noch ein bisschen Krokant.
Es ergibt sich also folgendes Bild:

Verschachtelungs-Prinzip

Interface – EisKomponente

Wie erreiche ich solch eine Verschachtelung in meiner Software? Beginnen wir mal mit einem Interface, das die Basis für alle weiteren Klassen darstellt. Es ist somit sowohl die Basis für das Eis als auch die Basis für die Toppings. Nennen wir dieses Interface einfach mal EisKomponente. Aus einer Bestellung ergibt sich also eine EisKomponente, in der alle Informationen enthalten sind. Diese Komponente sollte mindestens die Methoden getPreis und getBeschreibung zur Verfügung stellen, damit wir sowohl den Namen (z. B. „Vanilleeis mit Sahne und Krokant“) als auch den Preis des Produktes auf den Kassenzettel drucken können.

public interface EisKomponente {
    public double getPreis();
    public String getBeschreibung();
}

Die Eissorten implementieren also schlicht und einfach das Interface EisKomponente.

public class VanilleEis implements EisKomponente {
    @Override
    public double getPreis() {
        return 0.5;
    }
 
    @Override
    public String getBeschreibung() {
        return "Vanilleeis";
    }
}

Vanilleeis mit Sahne, Schokosoße UND Krokant

Bisher haben wir keine große Veränderung zu unserem ersten Software-Design. Es gibt immer noch für jedes Eis eine Klasse. Bleiben noch die Toppings… Was müssen wir nun bei den Topping beachten? Erinnern wir uns kurz an das Schaubild. Das Topping „Krokant“ enthält ein Topping „Schokosoße“. Dieses enthält wiederum ein Topping „Sahne“ und das Topping „Sahne“ enthält mein Eis. Es muss also eine Möglichkeit geschaffen werden, dass ein Topping sowohl ein anderes Topping, als auch ein Eis aufnehmen kann. Um beides speichern zu können, implementiert eine abstrakte Klasse Topping ebenfalls das Interface EisKomponente. So braucht ein Topping nur ein Objekt vom Typ EisKomponente zu speichern.

Zudem muss ein Topping den Preis für das innere Objekt plus den Preis für sich selbst zurückgeben können. Das Gleiche gilt für die Beschreibung. Hier eine mögliche Implementierung der Klasse Topping:

public abstract class Topping implements EisKomponente {
 
    private double preis;
    private String beschreibung;
    private EisKomponente innen;
 
    public Topping(double preis, String beschreibung, EisKomponente innen) {
        this.preis = preis;
        this.beschreibung = beschreibung;
        this.innen = innen;
    }
 
    public double getPreis() {
        return innen.getPreis() + preis;
    }
 
    public String getBeschreibung(){
        return innen.getBeschreibung() + " " + beschreibung;
    }
}

Zu guter Letzt fehlen nur noch die konkreten Implementierungen der Toppings. Hier am Beispiel der Klassen Sahne und Schokosoße.

public class Sahne extends Topping {
    public Sahne(EisKomponente innen) {
        super(0.3, "Sahne", innen);
    }
}
public class Schokososse extends Topping {
    public Schokososse(EisKomponente innen) {
        super(0.4, "Schokosoße", innen);
    }
}

Wie man sieht, ist der Aufwand zum Einfügen neuer Toppings sehr gering, da die Funktionalität bereits in der abstrakten Klasse Topping steckt.

Nun lässt sich einfach ein Vanilleeis mit Sahne und Schokosoße erstellen:

public static void main(String[] args) {
 
    EisKomponente meinEis = new VanilleEis();
    System.out.println(meinEis.getBeschreibung() + ": " + meinEis.getPreis());
 
    EisKomponente sahne = new Sahne(meinEis);
    System.out.println(sahne.getBeschreibung() + ": " + sahne.getPreis());
 
    EisKomponente schokososse = new Schokososse(sahne);
    System.out.println(schokososse.getBeschreibung() + ": " + schokososse.getPreis());
 
}

Das endgültige Design sieht nun wie folgt aus:

Die moderne Eisdiele von heute

Unsere Eisdiele besitzt nun ein leicht erweiterbares System, das uns die Arbeit im Alltag als Eisverkäufer erleichtert. In unserer Software kann man Eis mit verschiedenen Topping „dekorieren“. Genau aus diesem Grund spricht man bei dem, eben von uns umgesetzten Entwurfsmuster vom „Decorator Pattern“.

Dieses Entwurfsmuster findet sich beispielsweise in der Java-Bibliothek „java.io“ wieder. Insbesondere bei Streams wird auf das Decorator Pattern gesetzt. Nahezu jede Stream-Klasse kann wiederum einen Stream aufnehmen. So kann beispielsweise, zum Lesen von Java Objekten aus einer gezippten Datei, ein FileInputStream in einen GzipInputStream gegeben werden. Den GzipInputStream fügt man seinerseits in einen ObjectInputStream ein. Aus diesem können dann die Java Objekte geladen werden.

Wie man sieht, ist das Decorator Pattern nicht nur graue Theorie, sondern hat eine recht große praktische Relevanz.

Bildquelle: Flickr.com nach Lizenzbestimmungen Creative Commons “CC BY-SA 2.0”.