Das Decorator Pattern bietet die Möglichkeit einem Objekt dynamisch zusätzliches Verhalten hinzuzufügen. Dazu werden Klassen die einen gemeinsamen Basistyp haben, ineinander geschachtelt. Ein sehr bekanntes Beispiel für das Decorator Pattern ist in den Java IO Klassen zu finden.
Zum Lesen aus einem Stream, wird die Klasse InputStream, bzw. eine ihrer vielen Subklassen verwendet.
InputStream stream = new FileInputStream(new File("data.txt"));
In diesem Beispiel wird aus der Datei data.txt gelesen. Benötigt der Entwickler weitere Funktionalitäten , wie Base64 Encoding, Dekomprimieren , Checksummen-Berechnung oder die Verwendung eines Buffers, dann kann er weitere InputStream Implementierungen um den FileInputStream legen.
InputStream stream = new Base64(new BufferedInputStream(new FileInputStream(new File("data.txt"))));
Auf diese Weise arbeitet der Entwickler immer mit einem InputStream, kann aber je nach Bedarf weitere Funktionalitäten hinzufügen.
Häufig existieren Methoden, die zu großen Teilen aus der Prüfung ihrer Eingabeparameter bestehen. Obwohl sie mit Hilfe von Guard Clauses übersichtlich gestaltet werden können, stört dieser ganze Überprüfungscode. Er lenkt ab von der eigentlichen Aufgabe, die in der Klasse implementiert wurde.
public void export(Report report, User user) { if (report == null) { throw new IllegalArgumentException("report is missing"); } if (!user.isAdmin()) { throw new IllegalArgumentException("user is not admin"); } report.execute(user); }
Eine Menge Code und der Großteil prüft nur die korrekte Anwendung der Methode. Schön wäre es, wenn die die Überprüfung an anderer Stelle durchgeführt werden könnte. Dazu benötigen wir ein Interface, das von unserer Klasse implementiert wird.
public interface Exporter<T> { void export(T data, User user); } public class ReportExporter implements Exporter<Report> { public void export(Report report, User user) { report.execute(user); } }
Das erste Ziel unseres Refactorings haben wir erreicht, die Reduktion auf das Wesentliche der Methode ist gelungen. Die Verwendung eines generischen Typs ist nicht nötig, in diesem Beispiel erhöhen wir damit aber die Wiederverwendbarkeit der Decorator Klassen. Damit wir unsere Parameter prüfen können, benötigen wir eine Klasse, die den Admin Parameter prüft und dann die Methode der eingeschachtelten Klasse aufruft.
public class AdminExporter<T> implements Exporter<T> { private final Exporter<T> wrapped; AdminExporter(Exporter<T> wrapped) { this.wrapped = wrapped; } public void export(T data, User user) { if (!user.isAdmin()) { throw new IllegalArgumentException("user is not admin"); } wrapped.export(report, user); } }
Und eine Klasse, die unseren Report prüft
public class ExistingDataExporter<T> implements Exporter<T> { private final Reporter wrapped; NotNullExporter(Exporter wrapped) { this.wrapped = wrapped; } public void export(T data, User user) { if (data == null) { throw new IllegalArgumentException("data is missing"); } wrapped.export(report, user); } }
Durch die Trennung der Admin und der Report Prüfung erfüllen wir das Single-Responsibility-Prinzip und erhöhen die Chance auf Wiederverwendung. Da wir bei beiden Prüfungen den tatsächlichen Typ der Daten nicht benötigen, belassen wir den generischen Typ T und können diese Decorator nicht nur für Reports nutzen.
Nun müssen wir nur noch die die Instantiierung unserer Klasse ändern.
Exporter<Report> exporter = new ExistingDataExporter<>(new AdminExporter<>(new ReportExporter());
Durch den Einsatz der Guard Decorator haben wir nicht nur unseren Code übersichtlicher gestaltet, sondern ihn auch besser modularisiert und seine Wiederverwendbarkeit erhöht.