Erweiterung des Objektverhaltens mit dem Decorator Design Pattern
In der objektorientierten Programmierung wird das Verhalten eines Objekts normalerweise durch Vererbung oder Komposition erweitert. Diese Techniken führen jedoch Änderungen zur Compile-Zeit durch, wodurch sie zur Laufzeit unflexibel sind. Hier wird das Decorator Design Pattern besonders nützlich – es ermöglicht Modifikationen des Verhaltens eines Objekts dynamisch zur Laufzeit.
Stellen Sie sich vor, Sie müssen verschiedene Arten von Autos implementieren, die jeweils über spezifische Funktionen verfügen. Traditionell könnten Sie ein Car
-Interface erstellen, eine grundlegende Implementierung wie BasicCar
und diese dann auf SportsCar
und LuxuryCar
erweitern. Die Klassenhierarchie könnte folgendermaßen aussehen:
Car → BasicCar → SportsCar / LuxuryCar
Aber wenn Sie ein Auto erstellen möchten, das sowohl Sport- als auch Luxusmerkmale zur Laufzeit kombiniert, wird die Situation komplexer. Was ist, wenn Sie die Reihenfolge angeben müssen, in der diese Funktionen hinzugefügt werden? Bei zehn verschiedenen Autotypen wird das Management aller Kombinationen nur mit Vererbung fast unmöglich. Genau hier glänzt das Decorator Pattern.
Wie das Decorator Pattern funktioniert
Das Decorator Pattern ermöglicht es, das Verhalten eines einzelnen Objekts dynamisch hinzuzufügen, ohne das Verhalten anderer Objekte derselben Klasse zu beeinflussen. Dies ist besonders nützlich, wenn die Funktionalität eines Objekts zur Laufzeit erweitert werden muss, ohne die Struktur des Objekts zu ändern.
Um das Decorator Pattern zu implementieren, sind folgende Komponenten notwendig:
- Component Interface: Das Kern-Interface oder die abstrakte Klasse, die den Vertrag für alle Objekte definiert. In diesem Fall stellt
Car
das Component-Interface dar.
public interface Car {
public void assemble();
}
- Component Implementation: Eine grundlegende Implementierung des Component-Interfaces, wie z. B.
BasicCar
.
public class BasicCar implements Car {
@Override
public void assemble() {
System.out.print("Basic Car.");
}
}
- Decorator Class: Diese Klasse implementiert das Component-Interface und enthält eine Referenz zu einem
Car
-Objekt (der Komponente). Sie dient als Basisklasse für den Dekorator.
public class CarDecorator implements Car {
protected Car car;
public CarDecorator(Car c) {
this.car = c;
}
@Override
public void assemble() {
this.car.assemble();
}
}
- Concrete Decorators: Diese Klassen erweitern die Basisklasse des Dekorators und fügen zusätzliches Verhalten hinzu. Beispiele sind
SportsCar
undLuxuryCar
.
public class SportsCar extends CarDecorator {
public SportsCar(Car c) {
super(c);
}
@Override
public void assemble() {
super.assemble();
System.out.print(" Adding features of Sports Car.");
}
}
public class LuxuryCar extends CarDecorator {
public LuxuryCar(Car c) {
super(c);
}
@Override
public void assemble() {
super.assemble();
System.out.print(" Adding features of Luxury Car.");
}
}
Klassendiagramm des Decorator Patterns
Die Klassenstruktur für dieses Pattern kann wie folgt visualisiert werden:
- Car (Komponente)
- BasicCar (Konkrete Komponente)
- CarDecorator (Basis-Dekorator)
- SportsCar (Konkreter Dekorator)
- LuxuryCar (Konkreter Dekorator)
Testen des Decorator Patterns
Hier ist, wie Sie dieses Pattern in einem Testszenario verwenden können. Sie können Objekte von SportsCar
, LuxuryCar
erstellen oder sie zur Laufzeit dynamisch kombinieren:
public class DecoratorPatternTest {
public static void main(String[] args) {
Car sportsCar = new SportsCar(new BasicCar());
sportsCar.assemble();
System.out.println("\n*****");
Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new BasicCar()));
sportsLuxuryCar.assemble();
}
}
Ausgabe:
Basic Car. Adding features of Sports Car.
*****
Basic Car. Adding features of Luxury Car. Adding features of Sports Car.
Wie Sie sehen können, kann das Client-Programm verschiedene Autoobjekte zur Laufzeit erstellen und sogar die Reihenfolge der hinzugefügten Funktionen angeben.
Wichtige Vorteile des Decorator Patterns
- Runtime Flexibilität: Der größte Vorteil des Decorator Patterns ist seine Fähigkeit, das Verhalten von Objekten zur Laufzeit zu modifizieren, was im Vergleich zu Vererbungsdesigns eine unvergleichliche Flexibilität bietet.
- Wartung & Erweiterbarkeit: Das Hinzufügen oder Modifizieren von Verhalten wird einfacher, da Dekoratoren in verschiedenen Kombinationen zusammengestellt werden können, was das System skalierbarer macht.
Nachteile
- Erhöhte Komplexität: Da jede Funktion ihren eigenen Dekorator erfordert, kann die Anzahl der Dekorator-Klassen zunehmen, was das Management erschwert, wenn zahlreiche Funktionen hinzugefügt werden müssen.
Häufige Anwendungsfälle
Im Java-Bereich wird das Decorator Pattern häufig in den I/O-Klassen wie FileReader
, BufferedReader
und anderen verwendet, wo zur Laufzeit zusätzliche Funktionen zu Lesern und Schreibern hinzugefügt werden.