Gegeben sei folgendes Szenario: Ein Restaurant möchte seine beiden beliebtesten Gerichte "Wiener Schnitzel mit Pommes" und "Wiener Schnitzel mit Bratkartoffeln" modellieren. Der Preis und die Beschreibung sollen via Methoden abgefragt werden(Beispiel in Anlehnung an [VKBF], Seite 80ff.).
Unser Praktikant schlägt eine schnelle Lösung vor: Die zwei Gerichte werden abgebildet, indem für jedes Gericht eine eigene Klasse erstellt wird, die das Gerichtinterface implementiert und dessen Methoden getPreis() und druckeBeschreibung() jeweils ausprogrammiert.

Soweit so gut. Doch wie so oft in der Softwareentwicklung ändern sich die Anforderungen an unsere Modellierung: Das Restaurant möchte nun alle (!) Basisgerichte (Tofu, Garnelen, Hüftsteak, Wiener Schnitzel) mit allen möglichen Beilagen (Nudeln, Pommes, Bratkartoffeln, Salat, Suppe) modellieren. Bei unserer jetzigen Modellierung würde dies zu einer Klassenexplosion führen. 4 Basisgerichte * 5 Beilagen = 20 Klassen/Gerichte.
Die Nachteile sind offensichtlich und absolut inakzeptabel:
Doch bevor wir uns die Lösung anschauen, holen wir etwas weiter aus. Dazu sei hier ein wichtiges OO-Entwurfsprinzip rezitiert:
Offen/Geschlossen-Prinzip (Open/Closed):Gemeint ist, dass Erweiterungen (neue Verhalten etc.) ohne Änderungen an bestehenden Code in das System integriert werden können. Dies im Hinterkopf behaltend erinnern wir uns an ein weiteres stets aktuelles OO-Prinzip:
Identifiziere jene Aspekte, die sich ändern und trenne sie von jenen, die konstant bleiben.
Was ist konstant? Jedes Basisgericht und jede Beilage für sich.
Was ist variabel? Die Kombination von Basisgericht und Beilage.
Warum also nicht für jedes Basisgericht und für jede Beilage eine einzelne Klasse schreiben und diese dann dynamisch miteinander kombinieren?
Es zeichnet sich ab, dass das Offen/Geschlossen-Prinzip passt: Unsere Basisgericht- und Beilagenklassen sollen konstant sein und damit für Veränderung geschlossen, allerdings kann jedes Basisgericht durch diverse Beilagen erweitert werden und ist somit für Erweiterung offen. Das war jetzt sehr theoretisch. Wie realisieren wir dies?
Dazu begreifen wir Beilagen als "Wrapper", als Hülle, die wir um ein Basisgericht legen. Es sind Objekte, die ein Gericht (beispielweise eine Basisgericht) besitzen. also eine Instanzvariabale auf ein solches besitzen. Soll eine Salatbeilage seinen Preis ausgeben, so fragt er zuerst sein Hüftsteak nach dessen Preis und addiert anschließend seinen eigenen Salatpreis hinzu.
getPreis() von Salat delegiert die Preisberechnung an sein (Basis)Gericht und fügt seinen eigenen Preis dazu:
//Salatcode
private IGericht gericht;
public double getPreis() {
return gericht.getPreis() + 2.25;
}
Auch soll es möglich sein, Beilagen beliebig ineinander zu schachteln (Hüftsteak mit Nudeln und Salat). Folgendes Schema zur Visualisierung dieses Vorgangs:

Der Trick, den wir nun nutzen, ist, dass unsere Beilagen vom selben Typ sind, wie die Basisgerichte: IGericht. Logisch: ein Basisgericht und eine Beilage zusammen sind immer noch ein Gericht. Beilagen erweitern somit ebenso das Interface IGericht. Damit ist es möglich, dass eine Beilage sowohl ein Basisgericht als auch eine andere Beilage einpacken kann (diese wiederum ein anderes Gericht...).
Damit wäre das Decorator Pattern prinzipiell schon realisiert. Nur ist es üblich und zweckmäßig zwischen dem Interface IGericht und den konkreten Beilagen noch eine abstrakte Beilagenklasse zu schalten, die generischen Code für alle Beilagen enthalten kann. In unserem Fall wäre dies die Deklaration der Instanzvariable auf ein IGericht, sowie Methoden zum Setzen dieser Instanzvariable (Setter oder Konstruktor).
Quellcode Gerichtinterface:
public interface IGericht {
public double getPreis();
public void druckeBeschreibung();
}
Quellcode konkrete Basisgerichte:
class Hueftsteak implements IGericht {
public void druckeBeschreibung() {
System.out.print("Hüftsteak");
}
public double getPreis() {
return 13.0;
}
}
class Tofu implements IGericht {
public void druckeBeschreibung() {
System.out.print("Tofu");
}
public double getPreis() {
return 8.50;
}
}
class Garnelen implements IGericht {
public void druckeBeschreibung() {
System.out.print("Garnelen");
}
public double getPreis() {
return 13.50;
}
}
class WienerSchnitzel implements IGericht {
public void druckeBeschreibung() {
System.out.print("WienerSchnitzel");
}
public double getPreis() {
return 10.50;
}
}
Quellcode abstrakte Beilage:
public abstract class ABeilage implements IGericht {
protected IGericht gericht;
public ABeilage(IGericht pIGericht) {
gericht = pIGericht;
}
}
Quellcode konkrete Beilagen:
class Pommes extends ABeilage {
public Pommes(IGericht pIGericht) {
super(pIGericht);
}
public void druckeBeschreibung() {
gericht.druckeBeschreibung();
System.out.print(", Pommes");
}
public double getPreis() {
return gericht.getPreis() + 2.50;
}
}
class Salat extends ABeilage {
public Salat(IGericht pIGericht) {
super(pIGericht);
}
public void druckeBeschreibung() {
gericht.druckeBeschreibung();
System.out.print(", Salat");
}
public double getPreis() {
return gericht.getPreis() + 2.25;
}
}
class Nudeln extends ABeilage {
public Nudeln(IGericht pIGericht) {
super(pIGericht);
}
public void druckeBeschreibung() {
gericht.druckeBeschreibung();
System.out.print(", Nudeln");
}
public double getPreis() {
return gericht.getPreis() + 4.50;
}
}
class Suppe extends ABeilage {
public Suppe(IGericht pIGericht) {
super(pIGericht);
}
public void druckeBeschreibung() {
gericht.druckeBeschreibung();
System.out.print(", Suppe");
}
public double getPreis() {
return gericht.getPreis() + 1.50;
}
}
class Bratkartoffeln extends ABeilage {
public Bratkartoffeln(IGericht pIGericht) {
super(pIGericht);
}
public void druckeBeschreibung() {
gericht.druckeBeschreibung();
System.out.print(", Bratkartoffeln");
}
public double getPreis() {
return gericht.getPreis() + 1.50;
}
}
Nun können wir unsere Basisgerichte beliebig in Beilagen einpacken:
Beispielclient:
public static void main(String[] args) {
IGericht gericht = new Salat(new Nudeln(new Hueftsteak()));
gericht.druckeBeschreibung();
//Hüftsteak, Nudeln, Salat
System.out.println(" für "+gericht.getPreis() + " Euro");
// für 19.75 Euro
gericht = new Suppe(gericht);
gericht.druckeBeschreibung();
//Hüftsteak, Nudeln, Salat, Suppe
System.out.println(" für "+gericht.getPreis() + " Euro");
// für 21.25 Euro
}
Fassen wir die Vorteile unseres neuen Designs zusammen:
Es zeigt sich, dass durch den Einsatz des Decorator Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.
Nach dieser Einführung wird im folgenden Abschnitt das Decorator Design Pattern formalisiert, näher analysiert und diskutiert.
| Klasse | Decorator Teilnehmer |
|---|---|
| Basisgerichte (Hüftsteak, Wiener Schnitzel etc.) | Components (dt. Komponente) |
| Beilagen (Nudeln, Salat, Suppe etc.) | Decorators (dt. Dekorierer) |
Decorator:
"Erweitere ein Objekt dynamisch um Zuständigkeiten. Dekorierer bieten eine flexible Alternative zur Unterklassenbildung, um die Funktionalität einer Klasse zu erweitern."
([GoF], Seite 199)
Das Decorator Design Pattern ermöglicht das dynamische Hinzufügen von Fähigkeiten zu einer Klasse. Dazu wird die Klasse, dessen Verhalten wir erweitern möchten (Component, Komponente), mit anderen Klassen (Decorator, Dekorierer) dekoriert (vgl. engl. "to wrap": umhüllen). Das heißt der Decorator umschließt (enthält) die Component. Der Decorator ist vom selben Typ wie das zudekorierende Objekt, hat somit die gleiche Schnittstelle und kann an der selben Stelle wie die Component benutzt werden. Er delegiert Methodenaufrufe an seine Component weiter und führt sein eigenes Verhalten davor oder danach aus.
Eine Component kann mit beliebig vielen Decorators dekoriert werden, um so seine Fähigkeiten immer weiter auszubauen.
Realisiert wird das Pattern beginnend mit einer abstrakten Superklasse (oder einem Interface) AComponent. Sowohl die konkreten Components als auch alle Decorators sind von diesem Typ. AComponent definiert weiterhin beliebig viele abstrakte Methoden, die alle konkreten Subklassen implementieren müssen (hier operate()). Die konkreten Components erweitern AComponent direkt und implementieren ihr Basisverhalten. Die abstrakte Klasse ADecorator erweitert AComponent ebenfalls und hält zugleich eine Referenz auf ein (beliebiges) Objekt vom Typ AComponent. Konkrete Decorators erweitern schließlich ADecorator (sind damit auch AComponents) und implementieren ihr Zusatzverhalten (operate()). Darin rufen sie zum einen die Methode(n) (operate()) der AComponent (geerbt von ADecorator) auf und fügen davor oder danach ihr decoratorspezifisches Verhalten hinzu. Da es keine Rolle spielt, ob ihre AComponentreferenz nun eine konkrete Component oder wiederum ein anderer Decorator ist, können Decorators beliebig geschachtelt werden.
AComponent:
public abstract class AComponent {
//abstrakte Klasse oder Interface je nach Bedarf.
public abstract void operate();
}
ConcreteComponentA und ConcreteComponentB:
public class ConcreteComponentA extends AComponent {
public void operate() {
System.out.println("ConcreteComponentA operates.");
}
}
class ConcreteComponentB extends AComponent {
public void operate() {
System.out.println("ConcreteComponentB operates.");
}
}
ADecorator:
public abstract class ADecorator extends AComponent {
protected AComponent component;
//Konstruktor zum komfortablen Initiieren am Client
public ADecorator(AComponent pAComponent) {
component = pAComponent;
}
//operate() wird nicht implementiert.
}
ConcreteDecorator1, ConcreteDecorator2 und ConcreteDecorator3:
class ConcreteDecorator1 extends ADecorator {
public ConcreteDecorator1(AComponent pAComponent) {
super(pAComponent);
}
public void operate() {
//operate() der dekorierten Kompontente aufrufen vor
//oder nach der eigenen Funktionalitäst.
component.operate();
//eigene Funktionalitäst:
System.out.println("ConcreteDecorator1 operates!");
}
}
class ConcreteDecorator2 extends ADecorator {
//ConcreteDecorator2 fügt der Komponente einen neuen Zustand hinzu.
private int newState;
public ConcreteDecorator2(AComponent pAComponent) {
super(pAComponent);
newState = 999;
}
public void operate() {
component.operate();
System.out.println("ConcreteDecorator2 operates with a new State: " + newState);
}
}
class ConcreteDecorator3 extends ADecorator {
public ConcreteDecorator3(AComponent pAComponent) {
super(pAComponent);
}
public void operate() {
component.operate();
System.out.println("ConcreteDecorator3 operates with a new Operation: "+newOperation());
}
//ConcreteDecorator3 fügt der Komponente eine neue Methode hinzu und
//erweitert seine Schnittstelle nach auÃüen.
public int newOperation() {
return (int)(Math.random() * 20);
}
}
Beispielclient:
public static void main(String[] args) {
AComponent compA = new ConcreteComponentA();
compA.operate();
//ConcreteComponentA operates.
compA = new ConcreteDecorator1(compA);
compA.operate();
//ConcreteComponentA operates.
//ConcreteDecorator1 operates!
AComponent compB = new ConcreteDecorator3(new ConcreteDecorator2(new ConcreteComponentB()));
compB.operate();
//ConcreteComponentB operates.
//ConcreteDecorator2 operates with a new State: 999
//ConcreteDecorator3 operates with a new Operation: 12
}
Weitere anschauliche Beispiele:
In der Java API wird das Decorator Design Pattern an zahlreichen Stellen angewandt.
An zahlreichen Stellen in GUI-Bibliotheken werden GUI-Komponenten dekoriert. Ein typisches Beispiel ist die JScrollPane von Swing.


Soll eine JTextArea oder ein JTree Scrollbalken erhalten, so muss man das Objekt nur einer JScrollPane als Konstruktorparameter übergeben und das JScollPane-Objekt stattdessen (es ist ja auch eine Component) auf den Container (JPanel/JFrame) hinzufügen. Soll nun die Component gezeichnet werden, so fügt das JScrollPane-Objekt dem JTextArea-Objekt das nötige Verhalten für Scrollbalken hinzu.
Nutzung des Decorators JScrollPane:
JTextArea textComponent = new JTextArea(12,20);
JScrollPane scrollComponent = new JScrollPane(textComponent);
add(scrollComponent);
//oder add(new JScrollPane(new JTextArea(12,20));
Der berühmteste Nutzer des Decorator Design Patterns ist die Java IO. Hier sei stellvertretend die InputStream-Hierarchie vorgestellt. Bei OutputStream, Reader und Writer verhält es sich analog.
Die konkreten Basisstreams (Concrete Components) können durch andere Streams beliebig dekoriert werden, um ihnen damit zusätzliche Funktionalität zuzufügen. So fügt ein BufferedInputStream dem Basisstream einen Buffer hinzu und ermöglicht damit performantes Lesen. Dieser kann wiederum weiteren Decoratorstreams übergeben werden bis das gewünschte Verhalten erreicht wurde.

frei nach [VKBF], Seite 100
Interessant in der Java IO ist weiterhin der Decorator BufferedReader. Dieser Decorator implementiert nicht nur das Readerinterface (Componentinterface), sondern erweitert diese Schnittstelle noch um die Methode readLine(), mit der eine ganze Zeile statt einzelnen Zeichen gelesen werden kann. Um diese Methode nutzen zu können, muss der Client natürlich wissen, dass er es mit einem BufferedReader zu tun hat und nicht nur mit irgendeinen Reader.
Auch die Utilityklasse Collections nutzt das Decorator Pattern. Genau genommen, wird das Pattern von deren static nested ("inneren") Klassen, wie SynchronizedList oder UnmodifiableList genutzt. Diese Klassen, die selber das Listinterface implementieren (und damit selber Lists sind), nehmen eine List als Konstrukturparameter und leiten die entsprechenden Listmethoden an ihre List weiter, nehmen dabei jedoch Modifikationen vor.
UnmodifiableList leitet nur lesende Zugriffe (get(), hashCode(), indexOf()) an seine dekorierte List weiter. Auf alle Versuche schreibend auf die Liste zuzugreifen (add(), remove(), addAll()) wird eine UnsupportedOperationException geworfen.
SynchronizedList synchronisiert alle Zugriffe auf die List.
Codeausschnitt der static nested Klasse UnmodifiableList:
//viel Code...
static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list;
}
public int hashCode() {
return list.hashCode();
}
public E get(int index) {
return list.get(index);
}
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public boolean addAll(int index, Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
//viel Code...
}
Codeausschnitt der static nested Klasse SynchronizedList:
//viel Code....
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
public boolean equals(Object o) {
synchronized (mutex) {
return list.equals(o);
}
}
public int hashCode() {
synchronized (mutex) {
return list.hashCode();
}
}
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
public E remove(int index) {
synchronized (mutex) {
return list.remove(index);
}
}
public int indexOf(Object o) {
synchronized (mutex) {
return list.indexOf(o);
}
}
//viel Code...
}
Kommentare
Seite: 1 -