Das Decorator Pattern in einer Pizzeria

Mithilfe der Decorator Pattern, kann man eine existierende Klasse um weitere Funktionalität erweitern ohne davon explizit ableiten zu müssen.

Dabei werden Klassen desselben Basistyps so lange ineinander geschachtelt, bis man die gewünschte Funktionalität erreicht hat. Die eigentlichen Klassen kennen sich untereinander nicht und werden somit wiederverwertbarer. Man hat dadurch eine starke Kohäsion und eine lose Kopplung.

Ein kleines „Real-World“ Beispiel

Nachdem Luigi jahrelang seine Pizza nur für seine Freunde und Familie gebacken hat und nur positives Feedback bekommen hat, entschließt er sich schlussendlich eine Pizzeria zu eröffnen.

Jeder fängt mal klein. So bietet Luigi zunächst einmal drei Sorten von Pizza an. Margherita, Salami und Schinken.

Da Luigi ein moderner Pizzabäcker ist, bittet er dich eine digitale Speisekarte zu erstellen.

Gut, für 3 Pizzen braucht man ja auch nicht unbedingt eine große Klassenstruktur. Und so entsteht schnell eine Struktur mit genau 3 Klassen.

class Margherita { }
class SalamiPizza { }
class SchinkenPizza { }

Luigi ist zufrieden.

Doch wie flexibel ist diese Lösung wirklich?

Luigi bekommt immer mehr Kunden und bekommt immer mehr positives Feedback. So entschließt sich Luigi mehr Zutaten einzukaufen und diese zu kombinieren.

Anschließend hat Luigi 6 primäre Zutaten (Salami, Schinken, Parmaschinken, Thunfisch, Krabben und Sardellen) und 10 sekundäre Zutaten (Paprika, Peperoni, Zwiebeln, Oliven, Spinat, Champignons, Ananas, Knoblauch, Artischocken, Brokkoli).

Puh, das wird ein Stückchen Arbeit. OK, eine primäre und eine sekundäre Zutat auf einer Pizza frei kombiniert. Das macht bereits 60 mögliche Pizzen.

Aber was ist, wenn man mehrere Zutaten auf einer Pizza haben möchte? Meine Lieblingspizza, die Diavolo besteht z. B. aus 4 Zutaten. Gut das wäre ein Sonderfall. Also eine zusätzliche Pizza/Klasse.

Jetzt bin ich so einer, der keine Oliven mag. Sonderbestellungen wäre aber mit dem oberen Modell nicht möglich. Entweder müsste ich ohne Pizza und verärgert nach Hause gehen oder es müsste erst aufwendig eine neue Pizza erzeugt werden.

Und was ist, wenn Luigi jetzt beschließt seine Pizzen, um 50 Cent anzupassen? Man müsste jetzt also 62 Klassen anpassen.

Ich glaube, du verstehst langsam das Problem 🙂

Eine mögliche Lösung für das Problem wäre …

… es mit dem Decorator Pattern zu lösen.

Wir erstellen also zunächst ein Interface IPizza, eine Basisklasse für die Margherita und 16 Klassen für die Zutaten.

Eine primäre Zutat kostet 1 Euro und eine Sekundäre kostet 50 Cent.

abstract class Pizza
{
	public virtual double Preis { get; } = 0;
	public virtual string Zutaten { get; } = string.Empty;
}

abstract class BelegtePizza : Pizza
{
	private readonly Pizza _inner;
	
	protected BelegtePizza(Pizza pizza)
	{
		_inner = pizza;	
	}

	public override double Preis => Käserand.Preis + _inner.Preis;
	public override string Zutaten => _inner.Zutaten;
}

class Margherita : Pizza
{
	public override double Preis => 5;
	public override string Zutaten => "Tomatensoße, Käse";
}

class SalamiPizza : BelegtePizza
{
	public SalamiPizza(Pizza pizza) : base(pizza) { }
	public override double Preis => base.Preis + 1.00;
	public override string Zutaten => base.Zutaten + ", Salami";
}

class PeperoniPizza : BelegtePizza
{
	public PeperoniPizza(Pizza pizza) : base(pizza) { }
	public override double Preis => base.Preis + 0.50;
	public override string Zutaten => base.Zutaten + ", Peperoni";
}

class ZwiebelPizza : BelegtePizza
{
	public ZwiebelPizza(Pizza pizza) : base(pizza) { }
	public override double Preis => base.Preis + 0.50;
	public override string Zutaten => base.Zutaten + ", Zwiebeln";
}

class OlivenPizza : BelegtePizza
{
	public OlivenPizza(Pizza pizza) : base(pizza) { }
	public override double Preis => base.Preis + 0.50;
	public override string Zutaten => base.Zutaten + ", Oliven";
}

Das ganze Geheimnis ist dabei also, entweder ein Interface oder eine abstrakte Klasse zu definieren und diese einer anderen Klasse zu übergeben, die davon erbt.

Alle Methoden innerhalb des Interfaces werden einfach durchgereicht und ggf. leicht modifiziert, falls nötig.

Eine abstrakte Klasse hat noch zusätzlich den Vorteil, dass man wirklich nur die Methoden überschreibt, die man auch wirklich benötigt. Alle anderen werden einfach in Ruhe gelassen.

Das ist jetzt eine super flexible Lösung, die auch Sonderwünsche berücksichtigen kann.

class Luigis
{
	public Luigis()
	{
		Pizza margherita = new Margherita();
		Ausgabe("Margherita", margherita);

		Pizza salami = new SalamiPizza(margherita);
		Ausgabe("Salami", salami);

		Pizza schinken = new SchinkenPizza(margherita);
		Ausgabe("Schinken", schinken);

		Pizza diavolo = new SalamiPizza(margherita);
		diavolo = new PeperoniPizza(diavolo);
		diavolo = new ZwiebelPizza(diavolo);
		diavolo = new OlivenPizza(diavolo);
		Ausgabe("Diavolo", diavolo);

		Pizza hawaii = new SchinkenPizza(margherita);
		hawaii = new AnanasPizza(hawaii);
		Ausgabe("Hawaii", hawaii);

		Pizza speciale = new SalamiPizza(margherita);
		speciale = new SchinkenPizza(speciale);
		Ausgabe("Speciale", speciale);	
	}

	private void Ausgabe(string name, Pizza pizza)
	{
		Console.WriteLine($"{name} : {pizza.Preis:##.00} EUR");
		Console.WriteLine($" -> {pizza.Zutaten}");
		Console.WriteLine();
	}
}

Die Ausgabe wäre in etwa so:

Margherita : 5,00 EUR
-> Tomatensoße, Käse
Salami : 6,00 EUR
-> Tomatensoße, Käse, Salami
Schinken : 6,00 EUR
-> Tomatensoße, Käse, Schinken
Diavolo : 7,50 EUR
-> Tomatensoße, Käse, Salami, Peperoni, Zwiebeln, Oliven
Hawaii : 6,50 EUR
-> Tomatensoße, Käse, Schinken, Ananas
Speciale : 7,00 EUR
-> Tomatensoße, Käse, Salami, Schinken

Konkrete Anwendungsfälle für das Decorator Pattern

Mithilfe des Decorators, kann man dynamisch und zur Laufzeit Funktionalität hinzufügen oder auch wieder entfernen.

Beispielsweise kann man jede beliebige Klasse mit einem Logger dekorieren. Diesen kann man zentral an einer Stelle an und wieder abstellen.

Viele Textparser verwenden ebenfalls dieses Muster. So wird ein Text immer und immer wieder mittels regulären Ausdrücken maskiert und modifiziert, bis man am Ende ein gewünschtes Ergebnis hat.

So habe ich bereits vor über 15 Jahren dieses Muster angewandt, auch wenn ich noch nie etwas davon gehört habe.

Ein anderes Beispiel sind auch die Stream Klassen in der Java-Bibliothek. Hier werden durch das gezielte hinzufügen von Eigenschaften Stream-Objekte nach und nach aufgebaut.

Decorator Pattern – Die Nachteile

Es ist nicht alles Gold, was glänzt. Spontan fallen mir folgende Beispiele ein. Die Liste ist nicht vollständig, sind mir aber bereits untergekommen:

  • Durch die doch oft lange Aufrufketten, ist es schwierig die Fehler zu identifizieren und zu beseitigen. Welcher der angewandten 7 Decorator ist den jetzt für den Fehler verantwortlich? Richtig schräg wird es, wenn alle richtig funktionieren, aber eine Kombination aus zwei Objekten ein Fehlverhalten provoziert.
  • Durch die vielen Klassen kann man mal schnell den Überblick verlieren. Jede kleine Erweiterung bedarf einer neuen Klasse – mit dem ganzen drum herum. Wenn diese nicht richtig benannt oder vernünftig strukturiert wurden, fällt hinterher die Verwendung sehr schwer.
  • Durch die hohe Komplexität fällt es schwer ein Objekt sofort richtig zu instanziieren. Hier helfen anschließend nur Factory Methoden für eine passende und schnelle Instanziierung.
  • Eigentlich sollte es kein Unterschied machen in welcher Reihenfolge man dekoriert. Praktisch gibt es aber einen. So kann es passieren, dass eine bestimmte Reihenfolge durch einen Test abgedeckt wurde, eine andere aber zum Fehler führt.
  • Nehmen wir mal das Pizza-Beispiel von oben. Die Diavolo ist keine Salami-Pizza. Es ist eine Oliven-Pizza. Hätte ich die Salami als letztes darauf gelegt, wäre es eine Salami, aber keine Oliven-Pizza. Mit anderen Worten: Es ist keine Objektidentität gegeben. Würde ein Client explizit auf diesen Typ prüfen, gäbe es hier ein Fehler. Auf den Typen darf man sich bei einem dekorierten Objekt nicht mehr verlassen.
  • Dependency Injection funktioniert nur noch bedingt. Es reicht nicht aus, alle Klassen in ein Container zu stecken und hoffen, dass der Kernel es richtig macht. Hier muss man schon selber Hand anlegen und eine eigene Initialisierungsmethode schreiben.

Fazit – Decorator Pattern

Auch wenn die Liste der Nachteile zunächst lang wirkt, es lohnt sich das Pattern mal anzuschauen oder auch mal davon gehört zu haben.

In der Praxis verwende ich es persönlich nicht allzu oft. Einfach aus dem Grund, weil es zu meinem Stil und meinen Anforderungen nicht passt.

Aktuell habe ich jedoch ein Projekt am Laufen, wofür mir dieses Pattern direkt eingefallen ist. Ich habe ein paar Einstiegspunkte, die ich vom Aufruf klein halten möchte. Meine Hauptroutine möchte ich aber auch nicht mit zusätzlichen Krims-Krams überladen, sondern schön schlank halten. Am besten sollte die sich darum überhaupt nicht kümmern müssen.

Da fiel mir die Entscheidung doch gar nicht so schwer. Der Decorator 🙂