Guard Decorator

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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
InputStream stream = new FileInputStream(new File("data.txt"));
InputStream stream = new FileInputStream(new File("data.txt"));
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
InputStream stream = new Base64(new BufferedInputStream(new FileInputStream(new File("data.txt"))));
InputStream stream = new Base64(new BufferedInputStream(new FileInputStream(new File("data.txt"))));
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
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); }
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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);
}
}
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); } }
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class AdminExporter<T> implements Exporter<T> {
private final Exporter<T> wrapped;
AdminExporter(Exporter<T> wrapped)&nbsp; {
this.wrapped = wrapped;
}
public void export(T data, User user) {
if (!user.isAdmin()) {
throw new IllegalArgumentException("user is not admin");
}
wrapped.export(report, user);
}
}
public class AdminExporter<T> implements Exporter<T> { private final Exporter<T> wrapped; AdminExporter(Exporter<T> wrapped)&nbsp; { this.wrapped = wrapped; } public void export(T data, User user) { if (!user.isAdmin()) { throw new IllegalArgumentException("user is not admin"); } wrapped.export(report, user); } }
public class AdminExporter<T> implements Exporter<T> {
  private final Exporter<T> wrapped;

  AdminExporter(Exporter<T> wrapped)&nbsp; {
    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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class ExistingDataExporter<T> implements Exporter<T> {
private final Reporter wrapped;
NotNullExporter(Exporter wrapped)&nbsp; {
this.wrapped = wrapped;
}
public void export(T data, User user) {
if (data == null) {
throw new IllegalArgumentException("data is missing");
}
wrapped.export(report, user);
}
}
public class ExistingDataExporter<T> implements Exporter<T> { private final Reporter wrapped; NotNullExporter(Exporter wrapped)&nbsp; { this.wrapped = wrapped; } public void export(T data, User user) { if (data == null) { throw new IllegalArgumentException("data is missing"); } wrapped.export(report, user); } }
public class ExistingDataExporter<T> implements Exporter<T> {
  private final Reporter wrapped;

  NotNullExporter(Exporter wrapped)&nbsp; {
    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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Exporter<Report> exporter = new ExistingDataExporter<>(new AdminExporter<>(new ReportExporter());
Exporter<Report> exporter = new ExistingDataExporter<>(new AdminExporter<>(new ReportExporter());
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.