Nicht nur hübsch dekoriert, sondern auch funktional erweitert

Hallo Spaß-Coder.

In unserem letzten Artikel zur Serie über Entwurfsmuster haben wir uns die sog. Erzeugungsmuster angeschaut. Mit diesem Artikel wechseln wir in die Kategorie der Strukturmuster und werfen einen Blick auf den Dekorierer (engl. Decorator), ebenfalls aus der Sammlung der Gang of Four.

 

Wie funktioniert der Dekorierer?

Ebenso wie das traute Heim dekoriert und damit hübscher gestaltet wird (naja, die Aussage ist sicherlich nicht allgemeingültig…) können auch Implementierungen von Schnittstellen oder abstrakten Klassen dekoriert und dadurch um weitere Funktionalität ergänzt werden. Machen wir es direkt konkret und schauen uns ein Beispiel an – einen Geschirrspüler.

Der gemeinsame Nenner in allen Preisklassen von Geschirrspülern ist vermutlich in etwa durch diese Schnittstelle ausgedrückt: ich kann meinen Kram in den Geschirrspüler einräumen, ihn einschalten und im besten Fall mein sauberes Geschirr am Ende der Laufzeit wieder raus nehmen.

Bauen wir uns ein sehr einfaches Modell, das genau nur diese Funktionen erfüllt. Die modernen Geräte sind sicherlich mit dem Internet verbunden und schauen über die Webcam des Laptops automatisch nach, ob noch genügend sauberes Geschirr im Schrank ist, aber nun gut.

Bis hier hin noch nichts Spannendes. Lassen wir den Geschirrspüler nun laufen, passiert augenscheinlich nichts.

Die Konsole sieht dann wie folgt aus:

Wäre es nicht super, wenn der Geschirrspüler zumindest auf der Konsole Bescheid sagt, was er gerade so macht? Kein Problem, ändern wir doch unsere Implementierung und fügen diese Funktionalität hinzu. Was ist aber, wenn wir dies nicht können, z.B. da es eine Klasse aus einer fremden Bibliothek ist oder wir die Klasse nicht ändern wollen, weil sie so umfangreich, kompliziert und fehleranfällig ist?

D.h. wir haben den Wunsch, dem Geschirrspüler (also unserer Implementierung) neue Funktionalitäten hinzufügen, ohne die Klasse selbst zu verändern. Dazu bauen wir einen Dekorierer.

Ein neuer Geschirrspüler? Nicht ganz. Die Idee beim Dekorierer ist, dass dieser zunächst dieselbe Schnittstelle implementiert, wie die zu dekorierende Klasse. Per Konstruktor wird eine Implementierung der Schnittstelle mitgegeben, an welche später die tatsächliche Arbeit delegiert wird. In den implementierten Methoden der Schnittstelle wird immer dieselbe Methode auf der im Konstruktor gemerkten Implementierung aufgerufen und zusätzlich – und das ist der Trick an der Sache – weiterer Code ausgeführt. In unserem Beispiel erfolgt eine Ausgabe auf der Konsole.

Lassen wir nun den original Geschirrspüler sowie den LoggingDecorator laufen, sieht die Ausgabe auf der Konsole so aus:

Unsere Main-Methode ist dazu in der Form erweitert:

Hier sehen wir, dass der LoggingDecorator per Konstruktor die tatsächliche Implementierung erhält, die später die Arbeit machen soll, an die also die Aufrufe delegiert werden.

Da der Dekorierer dieselbe Schnittstelle implementiert ändert sich der Aufruf nicht. Der Aufrufende weiß ggf. gar nicht, dass es sich bei der Implementierung um einen Dekorierer handelt. Nur bei der Erzeugung muss natürlich einmalig festgelegt werden, welcher Geschirrspüler im Folgenden benutzt werden soll.

 

Wann ist der Dekorierer sinnvoll anzuwenden?

Im Abschnitt oben haben wir bereits erwähnt, das dieses Strukturmuster dann angewendet werden kann, wenn eine vorhandene Klasse um neue Funktionalität erweitert werden soll, die Klasse selbst aber nicht geändert werden kann, darf oder will. Ein Logging ist hier eine klassische Aufgabe für einen Dekorierer. Einige von euch denken nun vermutlich „Moment, das würde ich doch per AOP lösen“. Ja stimmt, Aspekt-Orientierte Programmierung ist ein anderer Ansatz, solche Anforderungen umzusetzen.

Wer das Muster nun weiter denkt, stellt vielleicht die Frage „Kann ich eigentlich einen Dekorierer dekorieren?“. Wir haben hier keine statische Ableitungshierarchie geschaffen, sondern über die jeweilige Implementierung der entsprechenden Schnittstelle die Möglichkeit, einzelne Funktionen in separate Dekorierer zu strecken und diese dann je nach Bedarf ineinander zu verschachteln. Jeder Dekorierer reicht die Aufrufe an den nächste Dekorierer weiter bis ganz innen die eigentliche Verarbeitung erfolgt. Damit lassen sich zur Laufzeit Entscheidungen treffen, welche Funktionen gerade benötigt werden und die Dekorierer entsprechend aufbauen bzw. zusammenstecken.

Ebenso wie das Hinzufügen von Funktionalität ist auch das Entfernen von Funktionalität möglich. Statt den Aufruf weiter zu delegieren kann eine eigene oder gar keine Implementierung durchlaufen werden.

Insbesondere mit steigender Anzahl von verwendeten Dekorieren ist die Suche nach einem auftretenden Fehler dadurch erschwert, dass die Ursache in einem der Dekorierer liegen kann, aber in welchem? Die Komplexität steigt und die Verwendung ist möglicherweise unklar: wann nutzte ich welchen Dekorierer. Habt ihr schon einmal mit den Java-IO-Streams und Readern gearbeitet? Diese sind nach dem Decorator Pattern aufgebaut. Das wird irgendwann unübersichtlich.

 

Zusammenfassung?

Wir haben uns in diesem Artikel ein Strukturmuster aus der Liste der Entwurfsmuster angeschaut, mit dem eine bestehende Klasse um neue Funktionalität erweitert werden kann, ohne die Klasse selbst zu verändern. Damit machen wir am bestehenden Code nichts kaputt und alle bisherigen Aufrufe funktionieren weiterhin so falsch oder richtig wie zuvor 🙂

 

Welchem Anlass könnt ihr euch noch vorstellen, euren Code zu dekorieren?

 

Eure Spaß-Coder

 

Dieser Artikel basiert neben unseren Erfahrungen auf den Ausführungen aus:

  • https://de.wikipedia.org/wiki/Decorator
  • https://de.wikipedia.org/wiki/Aspektorientierte_Programmierung

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.