Decorator (Design Pattern)

Aus Byte-Welt Wiki
Wechseln zu: Navigation, Suche

In unserer Designs ist es oft nützlich, das Verhalten eines Objektes zur Laufzeit zu erweitern, ohne vorhandenen Code ändern zu müssen. Wir können die Komposition und die Delegation benutzen, um neue Verhaltensweisen zur Laufzeit hinzuzufügen.

Das Decorator Design Pattern fügt einem Objekt dynamisch Verantwortung hinzu. Zwecks Erweiterung der Funktionalität eines Objekts, bietet das Decorator Design Pattern eine flexible Alternative zur Vererbung.

Das Decorator Design Pattern impliziert die Schaffung einer Reihe von Decorator-Klassen, welche die Komponenten umhüllen.

  • Decorator-Klassen sind vom gleichen Typ wie die Komponenten, die sie umhüllen (der Typ wird entweder durch Vererbung oder durch die Implementierung einer Schnittstelle festgelegt).
  • Die "Decorators" verändern das Verhalten ihrer Komponenten durch Hinzufügen neuer Features vor, nach oder anstelle von Aufrufen von Methoden der Komponente.
  • Wir können eine Komponente mit einer beliebige Anzahl von "Decorators" umhüllen.
  • Die "Decorators" sind transparent für den Kunden, sofern der Kunde auf dem abstrakten Obertyp der Komponente aufbaut, und nicht auf konkreten Untertypen.

Nehmen wir zum Beispiel den abstrakten Obertyp "Pet"

package tierschau;
 
public abstract class Pet
{
 
   private int age;
   private String species;
 
   public Pet(final int age)
   {
      this.age = age;
   }
 
   public int age()
   {
      return age;
   }
 
   public String species()
   {
      return species;
   }
 
   public void setSpecies(final String species)
   {
      this.species = species;
   }
 
   public abstract String laut();
 
   @Override
   public String toString()
   {
      return "Ich bin ein(e) " + species + "; ich bin " + age + " Jahre alt; ich kann '" + laut() + "' !";
   }
}

und den konkrete Untertyp "Katze", der von "Pet" abgeleitet ist:

package tierschau;
 
public class Katze extends Pet
{
 
   public Katze(final int age)
   {
      super(age);
      super.setSpecies(getClass().getSimpleName());
   }
 
   @Override
   public String laut()
   {
      return "miauen";
   }
}

Wir haben den Namen der konkreten Klasse in der Eigenschaft "species" der abstrakten Klasse "Pet" gespeichert (um zu vermeiden, dass wir uns zu einem späteren Zeitpunkt auf die konkrete Klasse "Katze" beziehen müssen).

Um das Verhalten eines "Pet" zu erweitern, führen wir den abstrakten Decorator "PetBehavior" hinzu. Erinnern wir uns daran, dass der Decorator den gleichen Typ hat wie die Komponente, die er umhüllt. Das ist der Grund, weshalb wir ihn von "Pet" ableiten:

package tierschau;
 
public abstract class PetBehavior extends Pet
{
 
   public PetBehavior(final int age)
   {
      super(age);
   }
}

Wir verwirklichen den abstrakten "Decorator" durch die zwei konkreten Implementierungen "PetSchnurrend" und "PetFauchend":

package tierschau;
 
public class PetSchnurrend extends PetBehavior
{
 
   private final Pet pet;
 
   public PetSchnurrend(final Pet pet)
   {
      super(pet.age());
      this.pet = pet;
      setSpecies(pet.species());
   }
 
   @Override
   public String laut()
   {
      return pet.laut() + ", schnurren";
   }
}


package tierschau;
 
public class PetFauchend extends PetBehavior
{
 
   private final Pet pet;
 
   public PetFauchend(final Pet pet)
   {
      super(pet.age());
      this.pet = pet;
      setSpecies(pet.species());
   }
 
   @Override
   public String laut()
   {
      return pet.laut() + ", fauchen";
   }
}

Beachten wir in diesen konkreten "Decorators":

  • die Anwesenheit der Eigenschaft "pet", die sich auf die Komponente bezieht, die vom Decorator umhüllt wird.
  • das Hinzufügen eines neuen Verhaltens in der Methode laut() nach dem Aufruf der Methode laut() der Komponente: return pet.laut() + ", schnurren", (in PetSchnurrend) und return pet.laut() + ", fauchen"; (in PetFauchend)

Schließlich zeigen wir hier die Startklasse der Anwendung "MeineTierschau":

package tierschau;
 
public class MeineTierschau
{
 
   public static void main(final String[] args)
   {
      Pet animal = new Katze(4);
      System.out.println(animal);
      animal = new PetSchnurrend(animal); //den Pet mit PetSchnurrend umhüllen
      System.out.println(animal);
      animal = new PetFauchend(animal); //den Pet mit PetFauchend umhüllen
      System.out.println(animal);
   }
}

Das Programm gibt die folgenden Zeilen auf der Konsole aus:

  • Ich bin ein(e) Katze; ich bin 4 Jahre alt; ich kann 'miauen' !
  • Ich bin ein(e) Katze; ich bin 4 Jahre alt; ich kann 'miauen, schnurren' !
  • Ich bin ein(e) Katze; ich bin 4 Jahre alt; ich kann 'miauen, schnurren, fauchen' !

Am Ende sehen wir, dass die Katze sowohl von PetSchnurrend als auch von PetFauchend umhüllt wurde. Auf diese Weise könnte sie mit einer beliebige Anzahl von Decorators umhüllt werden. Damit haben wir das Verhalten der Katze zur Laufzeit erweitert, ohne vorhandenen Code ändern zu müssen.

Die Java I/O Klassen sind übrigens auch nach dem Decorator-Prinzip aufgebaut. Wir finden dort zum Beispiel den abstrakten Obertyp InputStream mit den konkreten Untertypen FileInputStream, ByteArrayInputStream, ObjectInputStream, ... Die abstrakte Decorator-Klasse heißt FilterInputStream, die logischerweise auch von InputStream abgeleitet ist. Konkrete "Decorators" sind: PushbackInputStream, BufferedInputStream, DataInputStream, ... Man kann auch leicht eigene konkrete "Decorators" hinzufügen, etwa einen "LowercaseInputStream", der alle Großbuchstaben in Kleinbuchstaben umwandelt...