Decorator Pattern: Das Muster für dynamische Klassenerweiterungen
Wollen Sie eine bereits bestehende Klasse in einer objektorientierten Software um neue Funktionalitäten erweitern, haben Sie zwei verschiedene Möglichkeiten: Die einfache, aber auch schnell unübersichtliche Lösung ist, Unterklassen zu implementieren, die die Basisklasse in entsprechender Weise ergänzen. Als Alternative kann man eine Dekorierer-Instanz gemäß dem sogenannten Decorator Design Pattern verwenden. Das zu den 23 GoF-Design-Patterns zählende Muster ermöglicht eine dynamische Erweiterung von Klassen, während die Software läuft. Diese kommt dann ohne endlos lange, schwer zu überblickende Vererbungshierarchien aus.
Im Folgenden erfahren Sie, was genau es mit dem Decorator Pattern auf sich hat und welche Vor- und Nachteile dieses bietet. Zudem soll die Funktionsweise des Musters anhand einer grafischen Darstellung und eines konkreten Beispiels verdeutlicht werden.
Was ist das Decorator Pattern (Decorator-Entwurfsmuster)?
Das Decorator Design Pattern, kurz Decorator Pattern (dt. Decorator-Muster), ist eine 1994 veröffentlichte Musterstrategie für die übersichtliche Erweiterung von Klassen in objektorientierter Computersoftware. Nach dem Muster lässt sich jedes beliebige Objekt um ein gewünschtes Verhalten ergänzen, ohne dabei das Verhalten anderer Objekte derselben Klasse zu beeinflussen. Strukturell ähnelt das Decorator Pattern stark dem „Chain of responsibility“-Pattern (dt. Zuständigkeitskette), wobei Anfragen anders als bei diesem Zuständigkeitskonzept mit zentralem Bearbeiter von allen Klassen entgegengenommen werden.
Die Software-Komponente, die erweitert werden soll, wird nach dem Decorator-Entwurfsmuster mit einer bzw. mehreren Decorator-Klassen „dekoriert“, die die Komponente vollständig umschließen. Jeder Decorator ist dabei vom selben Typ wie die umschlossene Komponente und verfügt damit über die gleiche Schnittstelle. Dadurch kann er eingehende Methodenaufrufe unkompliziert an die verknüpfte Komponente delegieren, während er wahlweise zuvor bzw. anschließend das eigene Verhalten ausführt. Auch eine direkte Verarbeitung eines Aufrufs im Decorator ist grundsätzlich möglich.
Welchen Zweck erfüllt das Decorator Design Pattern?
Wie andere GoF-Muster, etwa das Strategy Pattern oder das Builder Pattern, verfolgt das Decorator Pattern das Ziel, Komponenten objektorientierter Software flexibler und einfacher wiederverwendbar zu gestalten. Zu diesem Zweck liefert der Ansatz die Lösung dafür, Abhängigkeiten zu einem Objekt dynamisch und – insofern erforderlich – während der Laufzeit hinzufügen bzw. entfernen zu können. Insbesondere aus diesem Grund stellt das Pattern eine gute Alternative zum Einsatz von Subklassen dar: Diese können eine Klasse zwar ebenfalls auf vielfältige Weise ergänzen, lassen jedoch keinerlei Anpassungen während der Laufzeit zu.
Eine Software-Komponente lässt sich durch beliebig viele Decorator-Klassen erweitern. Für zugreifende Instanzen bleiben diese Erweiterungen dabei gänzlich unsichtbar, sodass diese gar nicht mitbekommen, dass der eigentlichen Klasse zusätzliche Klassen vorgeschaltet sind.
Decorator Pattern: UML-Diagramm zur Veranschaulichung
Der Decorator bzw. die Decorator-Klassen (ConcreteDecorator) verfügen über die gleiche Schnittstelle wie die zu dekorierende Software-Komponente (ConcreteKomponente) und sind vom gleichen Typ. Dieser Umstand ist wichtig für das Handling der Aufrufe, die entweder unverändert oder verändert weitergeleitet werden, falls der Decorator die Verarbeitung nicht selbst übernimmt. Im Decorator-Pattern-Konzept bezeichnet man diese elementare Schnittstelle, die im Prinzip eine abstrakte Superklasse ist, als „Komponente“.
Das Zusammenspiel von Basiskomponente und Decorator lässt sich am besten in einer grafischen Darstellung der Beziehungen in Form eines UML-Klassendiagramms verdeutlichen. In der nachfolgenden abstrakten Abbildung des Decorator Design Patterns haben wir daher die Modellierungssprache für objektorientierte Programmierung verwendet.
Die Vorteile und Nachteile des Decorator Patterns im Überblick
Das Decorator-Muster bei der Konzeptionierung einer Software zu berücksichtigen, zahlt sich gleich aus mehreren Gründen aus. Allen voran steht das hohe Maß an Flexibilität, das mit einer solchen Decorator-Struktur einhergeht: Sowohl zur Kompilierungs- als auch zur Laufzeit lassen sich Klassen gänzlich ohne Vererbung um neues Verhalten erweitern. Zu unübersichtlichen Vererbungshierarchien kommt es bei diesem Programmieransatz folglich nicht, was nebenbei auch die Lesbarkeit des Programmcodes deutlich verbessert.
Dadurch, dass die Funktionalität auf mehrere Decorator-Klassen aufgeteilt wird, lässt sich außerdem die Performance der Software steigern. So kann man gezielt jene Funktionen aufrufen und initiieren, die man gerade benötigt. Bei einer komplexen Basisklasse, die alle Funktionen permanent bereitstellt, hat man diese ressourcenoptimierte Möglichkeit nicht.
Die Entwicklung nach dem Decorator Pattern bringt jedoch nicht nur Vorteile mit sich: Mit der Einführung des Musters steigt automatisch auch die Komplexität der Software. Insbesondere die Decorator-Schnittstelle ist in der Regel sehr wortreich sowie mit vielen neuen Begrifflichkeiten verknüpft und damit alles andere als einsteigerfreundlich. Ein weiterer Nachteil besteht in der hohen Anzahl an Decorator-Objekten, für die eine eigene Systematisierung zu empfehlen ist, um nicht mit ähnlichen Übersichtsproblemen wie bei der Arbeit mit Subklassen konfrontiert zu werden. Die oft sehr langen Aufrufketten der dekorierten Objekte (also der erweiterten Software-Komponenten) erschweren zudem das Auffinden von Fehlern und damit den Debugging-Prozess im Allgemeinen.
Vorteile | Nachteile |
---|---|
Hohes Maß an Flexibilität | Hohe Komplexität der Software (insbesondere der Decorator-Schnittstelle) |
Funktionserweiterung von Klassen ohne Vererbung | Einsteigerunfreundlich |
Gut lesbarer Programmcode | Hohe Anzahl an Objekten |
Ressourcenoptimierte Funktionsaufrufe | Erschwerter Debugging-Prozess |
Decorator Design Pattern: Typische Einsatzszenarien
Das Decorator Pattern liefert die Basis für dynamische und transparent erweiterbare Objekte einer Software. Insbesondere die Komponenten grafischer Benutzeroberflächen (GUIs) sind ein typisches Einsatzgebiet des Musters: Soll beispielsweise ein Textfeld mit einer Umrandung versehen werden, genügt ein entsprechender Decorator, der „unsichtbar“ zwischen das Textfeld-Objekt und den Aufruf geschaltet wird, um dieses neue Interface-Element einzufügen.
Ein sehr bekanntes Beispiel für die Umsetzung des Decorator-Entwurfsmusters sind die sogenannten Stream-Klassen der Java-Bibliothek, die für das Handling der Ein- und Ausgabe von Daten verantwortlich sind. Decorator-Klassen werden hier insbesondere dazu verwendet, um dem Datenstrom neue Eigenschaften und Statusinformationen hinzuzufügen oder um neue Schnittstellen bereitzustellen.
Java ist aber natürlich nicht die einzige Programmiersprache, in der der Einsatz des Decorator Patterns verbreitet ist. Auch folgende Sprachen setzen auf das Designmuster:
- C++
- C#
- Go
- JavaScript
- Python
- PHP
Praxisbeispiel für die Umsetzung des Decorator Patterns
Die Auflistung der Vor- und Nachteile zeigt, dass das Decorator Design Pattern nicht für jede Art von Software geeignet ist. Wo eine Klasse im Nachhinein noch verändert werden soll und insbesondere in Projekten, in denen dies nicht mithilfe von Subklassen zu realisieren ist, stellt das Designmodell jedoch eine erstklassige Lösung dar. Ein gutes Beispiel, das den Nutzen der „dekorierten“ Klassen unter Beweis stellt, liefert Marcel Schöni in folgendem Artikel seines ZKMA-Blogs.
Ausgangslage ist in diesem Fall eine Software, die die Namen von Personen über die abstrakte Klasse „Mitarbeiter“ abrufbar macht. Der erste Buchstabe der abgerufenen Namen ist jedoch immer kleingeschrieben. Da eine nachträgliche Anpassung unmöglich ist, wird die Decorator-Klasse „MitarbeiterDecorator“ implementiert, die über die gleiche Schnittstelle operiert und ebenfalls den Aufruf der Methode getName() ermöglicht. Zusätzlich erhält der Decorator eine Logik, die sicherstellt, dass der erste Buchstabe korrekterweise großgeschrieben wird. Das passende Code-Beispiel sieht dabei folgendermaßen aus:
public class MitarbeiterDecorator implements Person {
private Mitarbeiter mitarbeiter;
public MitarbeiterDecorator(Mitarbeiter mitarbeiter){
this.mitarbeiter = mitarbeiter;
}
public String getName(){
// rufe die Methode der Mitarbeiterklasse auf
String name = mitarbeiter.getName();
// Stelle hier sicher, dass der erste Buchstabe groß ist
name = Character.toUpperCase(name.charAt(0))
+ name.substring(1, name.length());
return name;
}
}