B) Decorator

Motivation

Manche Problemstellungen bringen es mit sich, dass man es mit vielen Objekten zu tun bekommt, die einander funktional sehr ähnlich sind. Eigentlich sind die Objekte sogar fast gleich, jedoch benötigt man die Möglichkeit verschiedene Anpassungen und Ergänzungen vorzunehmen. Als anschauliches Beispiel sollen hier Wölfe dienen, die zwei einfache Methoden mitbringen: sprich() und frissSchaf().
Aufgrund ihres natürlichen Lebensraum im hohen Norden erkälten sich Wölfe ab und zu, was sich durch vermehrtes Husten bemerkbar macht. Daneben sind einige Wölfe mitunter im Schafspelz unterwegs, um sich leichter an ihre Beute anschleichen zu können. Diese Wölfe verhalten sich vordergründig auch weniger drohend – obwohl sie letztendlich trotzdem Schafe und kein Gras fressen.
Ein erster, naiver Ansatz wird sein, die Besonderheiten der Wölfe in einer Vererbungshierachie auszudrücken. Der gesunde Wolf bildet die Wurzelklasse, weil die anderen seine Funktionalität erweitern sollen. Von Wolf werden dann die Klassen ErkaelteterWolf und WolfImSchafspelz abgeleitet. Sind damit alle Anforderungen abgedeckt? Natürlich nicht, denn was ist, wenn sich ein als Schaf verkleideter Wolf erkältet? Oder umgekehrt? Und was passiert, wenn ein weiterer „besonderer“ Wolf dazukommt?
Schon anhand dieses einfachen Beispiels zeigt sich, dass die Möglichkeitenvielfalt nicht zu handhaben ist – besonders, weil die meisten Klassen nur duplizierte Funktionalität aus anderen Klassen enthalten, wenn man den Vererbungsweg geht.

Decorator

Statt einer komplizierten Vererbungshierachie sollte man in einem Fall wie oben beschrieben, lieber zum Decorator-Pattern greifen. Ein Decorator wird um ein bestehendes Objekt herumgelegt und verändert sein Verhalten in einer bestimmten Art und Weise. Der Decorator hat dabei mit dem eingewickelten Objekt einen Basistyp gemeinsam – auf diese Weise kann der Decorator überall da benutzt werden, wo auch das bestehende Objekt benutzt werden könnte.
Mit ein wenig Code wird diese kryptische Prosa verständlich: Als erstes brauchen wir einen Basistyp, der generell in unserem Programm benutzt werden soll. Dafür schaffen wir uns ein Interface Wolf:

public interface Wolf {
  public String getName () ;
  public String sprich () ;
  public String frissSchaf () ;
}

Ein Wolf hat einen Namen, den er mit getName() verrät. Er kann außerdem Laute von sich geben („sprechen“) und Schafe fressen. Von diesem Interface leiten wir nun zunächst einmal den „normalen“ Wolf ab – also die Grundklasse ohne irgendwelche Modifikationen:

public class NormalerWolf implements Wolf {

  private String name;

  public NormalerWolf (String name) {
    this.name = name;
  }

  public String getName () {
    return name;
  }

  public String sprich () {
    StringBuilder sb = new StringBuilder("Grr");
    sb.append(" Ich bin ");
    sb.append(getName());
    sb.append(" und ich werde dich fressen!");
    sb.append(" Grr");

    return sb.toString() ;
  }

  public String frissSchaf() {
    return "mjam mjam *Schaf frisst*" ;
  }
}

Dieser Wolf kann erstmal nichts weiter als die Grundfunktionen ausführen. Er kann sich weder erkälten noch als Schaf verkleiden. Denn das erledigen ja die Dekoratoren für ihn. Der erste Decorator ist ErkaelteterWolf, welcher genau wie NormalerWolf Wolf implementiert:

public class ErkaelteterWolf implements Wolf {
  private Wolf wolf;

  public ErkaelteterWolf(Wolf wolf) {
    this.wolf = wolf;
  }

  public String frissSchaf() {
    return "*hust* *hust* " + wolf.frissSchaf();
  }

  public String getName() {
    return wolf.getName();
  }

  public String sprich( ) {
    return "*hust* *hust* " + wolf.sprich() ;
 }
}

An dieser Klasse sind gleich mehrere Dinge auffällig. Zunächst einmal ist der Konstruktor merkwürdig – der erkältete Wolf bekommt einen anderen Wolf übergeben und speichert diesen in einer Instanzvariablen ab. Dieser übergebene Wolf ist das Objekt, das der Decorator einwickelt und in seinem Verhalten modifiziert. Die Verhaltensmodifikation geschieht, in dem ErkalteterWolf die Methoden des „eingewickelten“ Wolfs aufruft und ihren Rückgabewert verändert. Eine Methode, die nicht verändert werden soll, wird einfach durchgereicht. Dies geschieht im Beispiel bei getName().
Der Wolf im Schafspelz ist mit diesen Informationen leicht zu verstehen:

public class WolfImSchafspelz implements Wolf {
  private Wolf wolf;

  public WolfImSchafspelz(Wolf wolf) {
    this.wolf = wolf;
  }

  public String getName() {
    return wolf.getName();
  }

  public String sprich() {
    return "*Grrr*, äh, *mäh*, ich bin " + getName()
        + " und ich fresse, äh, Gras!";
  }

  public String frissSchaf() {
    return "*scheinheilig* Aber, so was " + "würde ich doch nie tun! "
        + wolf.frissSchaf();
  }
}

Verwendet wird das Ganze wie folgt: Zunächst brauchen wir zwei ganz normale Wölfe, nennen wir sie „Fritz“ und „Harald“. An deren Erstellung ist nichts Besonderes:

Wolf harald = new NormalerWolf("Harald");
Wolf fritz = new NormalerWolf ("Fritz");

System.out.println(harald.sprich());
System.out.print("Harald frisst ein Schaf: ");
System.out.println(harald.frissSchaf());

System.out.println(fritz.sprich());
System.out.print("Fritz frisst ein Schaf: ");
System.out.println(fritz.frissSchaf());

Bei einem seiner Beutezüge hat Harald Zug abbekommen und sich erkältet:

harald = new ErkaelteterWolf(harald);
System.out.println(harald.sprich());
System.out.print("Harald frisst ein Schaf: ");
System.out.println(harald.frissSchaf());

In diesem Code-Abschnitt wird der „normale“ Wolf „Harald“ zum erkälteten Wolf. Er kann weiter wie bisher benutzt werden, da die beiden Klassen vom selben Typ erben. Das die Rückgabewerte der Methoden manipuliert werden, ist für den Anwender des Objekts nicht sichtbar, man sagt, es ist transparent.

Der grosse Vorteil von Dekoratoren gegenüber einer Vererbungshierachie ist, dass sie beliebig miteinander kombiniert werden können. Folgendes Codestück zeigt, wie dieser Effekt erreicht werden kann:

fritz = new WolfImSchafspelz(fritz);
fritz = new ErkaelteterWolf(fritz);
System.out.println(fritz.sprich());
System.out.println(fritz.frissSchaf());

Fritz tarnt sich zunächst als Schaf, um sich seiner Beute leichter nähern zu können. Trotz des dicken Extrapelzes erkältet er sich jedoch kurze Zeit später. Diese unglückliche Verkettung der Umstände läßt sich mit Dekoratoren einfach und verständlich ausdrücken, den ein WolfImSchafspelz ist immer noch ein Wolf und kann deshalb genauso in einen zweiten Dekorator eingewickelt werden.

Dieser Artikel wurde uns von Tobias Demuth zur Verfügung gestellt. Tobias Demuth ist Dipl.-Wirtschaftsinformatiker (FH) und arbeitet als Anwendungsentwickler bei der Viessmann IT Service GmbH in Nordhessen.

5 Replies to “B) Decorator”

  1. info-bachelor

    Hallo zusammen, ein sehr netten Blog habt Ihr hier und ich werde mich hier sicherlich ein wenig weiter umsehen. Ich beschäftige mich auf meiner Seiten meist in Englisch, aber auch in Deutsch mit ganz ähnlichen Themen. Ich finde hier die Kapselung in Java ganz schön aufgezeigt + ein lustiges Beispiel 🙂

    Weiter so!

    Viele Grüße

    Henrik

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.