Suche

Design Pattern auf dem Prüfstand: Mehrwert oder Mühsal?

Herbert Dowalil

© Shutterstock.com / bosotochka

23 Jahre ist es nun her, dass die berühmte Viererbande ihr Buch zum Thema Design Pattern veröffentlicht hat. Nicht umsonst gilt es als das Standardwerk zum Thema. Welche typischen Fallstricke können beim Einsatz von Entwurfsmustern auftreten? Und wie umgeht man diese?

Design Pattern sind aus dem Vokabular unserer Community kaum noch wegzudenken. Definiert sind sie laut Wikipedia als bewährte Lösungsschablonen wiederkehrender Entwurfsprobleme. Durch den Einsatz solcher Muster verspricht man sich üblicherweise die folgenden beiden positiven Effekte:

  1. Sie bieten angemessene Problemlösungsstrategien, die trotz ihrer Universalität für häufig wiederkehrende Problemstellungen angewendet werden können. Dabei wird üblicherweise auch eine gewisse Flexibilität der Lösung erreicht, wodurch eine Langlebigkeit der Lösung gewährleistet werden soll.
  2. Da die Pattern den meisten Developern geläufig sind, werden sie im Code wiedererkannt. Sobald die Sourcen von jemand anderem als dem Autor verstanden werden müssen, ist das einfacher möglich, da die ursprüngliche Intention sofort erkennbar ist.

Von der Viererbande, oder auch GoF (für Gang-of-Four) genannt, wurden die Design Pattern in ihrem berühmten Buch dabei in die folgenden drei Kategorien eingeteilt:

  • Erzeugung/Creational: Dabei geht es immer in der einen oder anderen Form um die Instanziierung von Objekten.
  • Strukturmuster/Structural: Dienen als Schablonen für den Aufbau von einzelnen Komponenten und deren Beziehungen zueinander.
  • Verhalten/Behavioral: Befassen sich üblicherweise mit den Abläufen des Zusammenspiels einzelner Komponenten.

So weit so gut. In der Praxis als Coach bin ich aber oft auf dieselben Probleme gestoßen, über die Development-Teams beim Einsatz von Entwurfsmustern stolpern. Beginnen wir mit einem Code Snippet, das mir einmal auf ganz ähnliche Art und Weise untergekommen ist, wie hier im folgenden Listing 1:

public class DogFactory {

      public static Dog createDog(Criteria criteria) {
          
            switch (criteria) {
            case POODLE:
                return new Poodle();
            case ROTTWEILER:
                return new Rottweiler();
            case SIBERIANHUSKY:
                return new SiberianHusky();
            default:
                return null;
            }
      }
}

Das sollte eine Umsetzung des Factory Pattern sein, wie am Suffix der Klasse leicht erkennbar ist. Allerdings muss sich der ursprüngliche Autor die Frage gefallen lassen, welches Ziel er mit der Erstellung dieser Klasse eigentlich im Auge hatte. Die Lösung besitzt nämlich keinerlei Mehrwert, da es sich um eine 1-zu-1-Beziehung zwischen Entscheidungskriterium und erzeugter Objektinstanz handelt. Es scheint, als wäre dieser missglückte Einsatz des Pattern ein Ziel mit Selbstzweck gewesen.

Zu einem solchen Fall von Overengineering kommt es gar nicht so selten. Üblicherweise ist der Fehler der, dass in erster Linie über das Design Pattern nachgedacht wurde. Die Frage nach der Angemessenheit für die konkrete Problemstellung wurde aber offensichtlich übersehen. Der Einsatz eines Design Pattern bedeutet nämlich auch immer ein höheres Level an Abstraktion. Es bringt also eine zusätzliche Komplexität mit sich, für die es wiederum einen gewissen Mehrwert geben muss. Ist dieser Mehrwert nicht gegeben, ist vom Einsatz des jeweiligen Pattern abzuraten. Im konkreten Fall wäre ein sinnvolles Szenario eines, wie im folgenden Listing 2 dargestellt:

public class DogFactory {

      public static Dog createDog(Criteria criteria) {
          
            switch (criteria) {
            case SMALL:
                return new Poodle();
            case BIG:
                return new Rottweiler();
            case WORKING:
                return new SiberianHusky();
            default:
                return null;
            }
      }
}

Hier ist die Anwendung des Pattern tatsächlich sinnvoll. Es handelt sich dabei um das sogenannte Simple Factory Pattern, bei dem es darum geht, die Entscheidung betreffend der Erzeugung einer konkreten Objektinstanz, abhängig von einem Entscheidungskriterium, an einer Stelle zu kapseln. Wenn Sie als Züchter und Verkäufer der Hunde also später bemerken sollten, dass eine der erzeugten Hunderassen für den jeweiligen Anwendungsfall doch nicht so geeignet ist wie gedacht, so kann man diese an einer zentralen Stelle auswechseln.

In der folgenden Auflistung gebe ich einen Überblick über die häufigsten Design Pattern und dafür jeweils typische Anwendungsfälle. Sollte die damit erreichte Abstraktion nicht das sein, was auch tatsächlich gewünscht ist, so beschreibe ich zudem, welche einfachen Mittel von Java und Java EE genügen, und dabei gar nicht so selten die bessere Lösung sind.

Lesen Sie auch: Spaghetti-Code? Nein danke! Eigene Design Pattern für besseren Code

Decorator

Typ: Structural

Anwendung: Da in Java keine Möglichkeit besteht, von mehr als einer Klasse zu erben, kann man mithilfe dieses Musters ein Interface extenden und somit erst zur Runtime die konkrete Instanz wählen, die um Funktionalität erweitert werden soll.

Umgesetztes Prinzip: Dependency Inversion (das D in „SOLID“)

Alternative: In Java EE kann man auf die @Decorator-Annotation setzen. Ansonsten bieten sich in Java Vererbung mittels extends oder eine simple Wiederverwendung durch Methodenkapselung an.

Adapter

Typ: Structural

Anwendung: Eine ausgehende Schnittstelle einer Komponente soll mit einer eingehenden Schnittstelle einer anderen Komponente verbunden werden, welche die benötigte Funktionalität anbietet. Diese sind aber nicht miteinander kompatibel. Man kann (oder möchte) aber keine Abhängigkeit zur neuen Komponente in die ursprüngliche einbauen, und verbindet beide daher über einen Adapter, der die Konvertierung übernimmt. Als Spezialfälle für dieses Pattern seien noch Proxy, Bridge und Mediator erwähnt. Umgesetztes Prinzip: Separation Of Concerns.

Alternative: Einfache Wiederverwendung mit Java Sprachmitteln

Facade

Typ: Structural

Anwendung: Eine Fassade bündelt den Zugriff auf ein Modul in einer dafür vorgesehenen Komponente. Das kann einerseits sein, um im Sinne des Information Hiding Principles die konkrete Implementierung vor der Außenwelt zu verbergen, oder andererseits erfolgen, um einen einfachen Zugang für ein ansonsten komplexes Modul bereitzustellen.

Umgesetztes Prinzip: Information Hiding

Alternative: In Java EE würde ein Export als REST Interface mittels JAX-RS automatisch eine Fassade darstellen. Ansonsten kann es oft schon genügen, die Sichtbarkeit von Klassen einzuschränken, indem man sie package protected macht, wenn dies möglich ist. Ab Version 9 von Java ist die Moduldefinition von Projekt Jigsaw eine Alternative.

Observer

Typ: Behavioral

Anwendung: Eine Komponente, die einen gewissen State enthält, soll Auskunft darüber geben, sollte dieser sich ändern. Im Sinne einfacher Erweiterbarkeit ist gewünscht, dabei beliebig viele andere Komponenten darüber zu informieren. Ein Observer ist somit ein Spezialfall eines Event Listeners.

Umgesetztes Prinzip: Open Closed

Alternative: In Java EE stellen die Events des CDI-Standards zur Verfügung. Oft reicht auch ein einfacher Methodenaufruf.

Visitor

Typ: Behavioral

Anwendung: Es gibt eine heterogene Menge an Objektinstanzen, deren Auffinden nicht trivial ist. Dabei möchte man unterschiedliche Operationen an diesen ausführen, die jeweils in einer eigenen Klasse (dem eigentlichen Besucher oder Visitor) gekapselt werden sollen. Man möchte die Logik aber nicht in den aufzufindenden Klassen implementieren. Es wird dabei voraussichtlich mehr als einen konkreten Besucher geben.

Umgesetztes Prinzip: Separation Of Concerns, Open Closed

Alternative: Streams ab Java 8 oder ansonsten Collections.iterator kombiniert mit einem switch/case-Statement.

Simple Factory

Typ: Creational

Anwendung: Auf Basis eines Auswahlkriteriums wird eine bestimmte Klasseninstanz erzeugt. Das wird dabei an mehr als einer Stelle benötigt und soll für spätere einfache Änderbarkeit an einer zentralen Stelle gekapselt werden.

Umgesetztes Prinzip: Separation Of Concerns

Alternative: In CDI kann auf @Produces in Kombination mit Qualifier Annotations gesetzt werden. In Java ist eine einfache Instanziierung mit = new (); möglich.

Factory Method bzw. Abstract Factory

Typ: Creational

Anwendung: Trennt die Erzeugung konkreter Instanzen von Logik, die diese konkreten Instanzen benötigt. Somit kann die Logik später jederzeit mit neu zu erstellenden Klassen, die dieselben Interfaces wie das ursprüngliche Set an Klassen implementieren, ebenfalls verwendet werden.

Umgesetztes Prinzip: Separation Of Concerns, Open Closed Alternative: Siehe Simple Factory

Zwischenfazit

Kommen wir also noch einmal zum anfangs erwarteten Nutzen Nr. 1 zurück, und fragen, ob uns Design Pattern tatsächlichen einen Mehrwert bieten, was die Lösung üblicher Problemstellungen angeht. Diese Frage ist zweifelsohne zu bejahen. Allerdings tut man gut daran, immer den konkreten Nutzen bei jedem Einsatz auch zu hinterfragen um nicht in die Falle der „Patternitis“ zu tappen. Erfolg definiert sich am Ende des Tages nicht durch die Anzahl der eingesetzten Muster im Code, sondern in erster Linie aus der Effektivität bei dessen Umsetzung und Wartung. Sollte diese durch den Einsatz eines Design Pattern nicht steigen, so gibt es immer einfachere Alternativen zur Umsetzung.

Wie sieht es aber mit dem zweiten Nutzen aus, den man sich vom Einsatz der Design Patterns verspricht? Nämlich, dass andere, die den Code lesen, diesen angeblich besser verstehen können, weil die ursprüngliche Intention schneller klar wäre. Wenn Sie bitte nochmals einen Blick auf die DogFactory in Listing 2 werfen, so wird dort durch die Kapselung des switch/case Statements erreicht, dass später jederzeit die konkrete Hunderasse, die für einen gewissen Anwendungsfall vorgesehen ist, an dieser einen zentralen Stelle ausgewechselt werden kann. Tatsächlich kann aber dieser Code immer noch zu Verwirrung führen, weil es nämlich mehrere unterschiedliche Varianten des Factory Patterns gibt. Dabei kommt noch dazu, dass die anderen beiden Varianten (nämlich Factory Method und Abstract Factory) ein anderes Ziel verfolgen als die hier in Listing 2 angeführte Simple Factory. Zur Abgrenzung hier in Listing 3 ein Beispiel für eine korrekte Umsetzung des Factory Method Patterns:

/**
 * GOF: 107
 */
public class MealFactory {
     
    @PatternFactoryMethod
    protected Spice createSpice() {
        return new Pepper();
    }
     
    @PatternFactoryMethod
    protected Utensil createUtensil() {
        return new createFryingPan();
    }
     
    public Meal prepare() {
         
        Utensil utensil = provideUtensil();
        Spice spice = provideSpice();
         
        // now do whatever steps are needed to prepare the meal...
    }
}

Die Idee dabei ist, in einer Klasse, die von dieser erbt, die Factory-Methoden zu überschreiben, während die eigentliche Logik (in diesem Fall die Zubereitung) gleich bleibt, siehe Listing 4:

public class MealFactoryAsianStyle extends MealFactory {

    @Override
    protected Spice createSpice() {
        return new Curry();
    }

    @Override
    protected Utensil createUtensil() {
        return new Wok();
    }
}

Dabei geht es also darum, das selbe Rezept mit unterschiedlichen Utensilien und Gewürzen zubereiten zu können, ohne dabei das eigentliche Rezept verändern zu müssen. Zur besseren Abgrenzung zu den anderen Varianten des Factory Pattern haben wir außerdem zu folgenden Hilfsmitteln im Code in Listing 3 gegriffen:

  • Im JAVADOC der Klasse MealFactory gibt es den Hinweis GOF:107. Dieser bezieht sich auf die Seite der englischen Originalausgabe des Design-Pattern-Buches der Gang-of-Four. Somit ist klar definiert, welches wiederverwendbare Entwurfsmuster der ursprüngliche Autor des Codes im Sinn hatte.
  • Die beiden Methoden createSpice und createUtensil wurden mit der Annotation @PatternFactoryMethod versehen.

Die erste hier beschriebene Variante zur Dokumentation des eingesetzten Design Patterns (GOF: 107) trifft man besonders häufig in Open-Source-Projekten an. Hinter der zweiten wiederum steckt ein Vorgehen, wie ich es jedem Development-Team empfehle. Zuerst einigt man sich auf ein gewisses Set an Design Pattern, von dem man annimmt, dass sie am häufigsten zum Einsatz kommen werden. Danach sucht man nach Referenzimplementierungen im Internet, wie es sie u.a. auf GitHub gibt. Am Schluss erstellt man ein kleines Java-Modul als .jar File zum Reuse, wo für jedes Pattern eine Annotation definiert ist, deren Zweck es ist, den Einsatz eines bestimmten Patterns im Code zur besseren Wartbarkeit zu dokumentieren. Die in diesem Beispiel in Listing 3 verwendete Annotation könnte dabei wie hier in Listing 5 definiert sein:

/**
 * Pattern Factory Method
 * Reference Implementation: https://en.wikipedia.org/wiki/Factory_method_pattern
 * Purpose: Should be used to separate a certain algorithm from the instantiation of Classes needed for that algorithm. So the algorithm can be reused with other Sets of Class instances created in a Subclass of the original Class.
 */
@Documented
@Retention(value=RetentionPolicy.SOURCE)
@Target(value={ElementType.METHOD})
public @interface PatternFactoryMethod {

}

Eine solche Erstellung eines eigenen teaminternen Musterkataloges hat gleich zwei Vorteile: Beim Erstellen der Library mit den Annotations kommt das Team nicht umhin sich um das Thema Design Pattern Gedanken zu machen und wird sich dabei automatisch bezüglich des Einsatzes abstimmen müssen. Des Weiteren wird Code durch Verwendung der dabei erstellten Annotations besser dokumentiert und Missverständnisse über das konkret verwendete Muster sind danach so gut wie ausgeschlossen.

Builder Pattern

Wie wichtig die Erstellung eines solchen Kataloges von Entwurfsmustern ist, zeigt die Situation bei einem anderen Vertreter der Creational Pattern, nämlich dem Builder Pattern. Diese ist sogar noch verwirrender als bei den verschiedenen Abwandlungen des Factory Pattern, weil es unterschiedliche Interpretationen davon gibt. Einerseits gibt es die Definition im Klassiker der Viererbande (GOF:97). Bei dieser geht es darum, komplexe Logik, die zur Erzeugung bestimmter Klasseninstanzen notwendig ist, von der konkreten erzeugten Instanz zu trennen. Dieselbe Logik kann dann verwendet werden, um andere Instanzen zu erzeugen. Für ein Beispiel sei an dieser Stelle einfach auf die Wikipedia-Seite zum Pattern verwiesen. Mir persönlich ist noch nie ein Anwendungsfall für dieses Muster in Java untergekommen oder es war der Einsatz des Factory Method Patterns einfach passender. Noch zur Abgrenzung zum Factory Method Pattern: Dabei geht es um die Logik, die mit den erzeugten Instanzen arbeitet, und nicht wie bei der GoF:97 Builder-Variante um die Logik zur Erzeugung selbst.

Lesen Sie auch: DDD-Patterns – Bausteine für erfolgreiches Domain-driven Design

Googelt man nach „Builder Pattern“, so findet man beinahe genauso oft eine andere Interpretation, die ich in weiterer Folge einfach als Variante 2 bezeichnen werde. Bei dieser werden völlig andere Ziele verfolgt. Meist setzt man diese Variante des Builder Patterns ein, wenn eine Instanz einer Klasse erzeugt wird, die nach der erstmaligen Instanziierung nicht mehr veränderbar, also „immutable“, sein soll. Das ist oft aus den gleichen Gründen sinnvoll, weshalb in Java auch Klassen wie String oder Integer immutable sind. Code, der diese Klassen verwendet, wird dadurch generell weniger Fehleranfällig. Im einfachen Fall bieten Sie einfach entsprechende Konstruktoren an, wo alle nötigen Daten mitgegeben werden. In etwas komplexeren Fällen sieht der Code dann aber wie folgt aus:

@Immutable
public final class Pizza {
   private Set<String> ingredients;
   private boolean cheesyCrust;
   private boolean folded;
 
   private Pizza(boolean cheesyCrust, boolean folded, Set<String> ingredients) {
      super();
      this.ingredients = ingredients;
      this.cheesyCrust = cheesyCrust;
      this.folded = folded;
   }
 
   @PatternBuilder(Pizza.class) 
   static class Builder {
 
      private Set<String> ingredients = new HashSet<String>();
      private boolean folded = false;
      private boolean cheesyCrust = false;
 
      public Builder addIngredient(String newIngredient) {
         ingredients.add(newIngredient);
         return this;
      }
 
      public Builder cheesyCrust() {
         this.cheesyCrust = true;
         return this;
      }
 
      public Builder fold() {
         this.folded = true;
         return this;
      }
 
      // convenient method to quickly create Pizza cardinale
      public Builder cardinale() {
         ingredients.clear();
       addIngredient("Tomaten").addIngredient("Käse").addIngredient("Schinken");
         return this;
      }
 
      public Pizza build() {
         return new Pizza(this.cheesyCrust, this.folded, this.ingredients);
      }
 
   }
 
   public void eat() {
      // ...
   }
}

Die einzelnen Methoden der internen Builder-Klasse (wie cheesyCrust) liefern jeweils eine Referenz auf sich selbst Retour, wodurch eine flüssige und selbsterklärende Erzeugung einer Instanz der Pizza-Klasse möglich wird. Die Erzeugung einer Pizza Cardinale mit extra Mais und käsiger Kruste würde dann wie folgt aussehen:

Pizza myPizza = (new Pizza.Builder()).cardinale().addIngredient("Mais").

cheesyCrust().build();

Diese Variante des Builder Pattern ist jedem Java-Entwickler geläufig, der schon mal einen StringBuilder verwendet hat. Das ist nichts anderes als ein solcher Builder für die String-Klasse, welche ja ebenfalls immutable ist. In JAX-RS wird eine Response ebenfalls mit einem solchen Builder erzeugt.

Lesen Sie auch: Resilient Software Design: Ein Jahr später …

JSR 305 – Design by contract

Bei dieser Gelegenheit möchte ich noch kurz auf die zu Beginn von Listing 6 verwendete Annotation @Immutable eingehen. Dabei handelt es sich um einen im JSR 305 definierten Standard im Java-Bereich zum Thema Design by Contract. Diese Annotation teilt also jedem Benutzer der Klasse mit, dass eine erstmal erzeugte Instanz nicht mehr änderbar ist. Durch die Verwendung dieses Standards ist durch Unterstützung von Tools wie FindBugs sogar möglich, fehlerhaften Code automatisch zu entdecken. Ein paar Beispiele von Annotations, wie sie im JSR 305 definiert wurden:

  • @CheckReturnValue – Eine Methode verhält sich ähnlich wie String.trim() und verändert nicht den internen State der Instanz, sondern liefert stattdessen immer eine neue Objektinstanz zurück. Ist eine Methode mit dieser Annotation versehen, kann FindBugs auf das irrtümliche Ignorieren der returnierten neuen Instanz hinweisen.
  • @Syntax(„JSON“) – Mit dieser Annotation versehen, kann man den Inhalt eines Strings näher spezifizieren.
  • @WillClose bzw. @WillNotClose – Hiermit kann dem Benutzer einer Methode mitgeteilt werden, ob die übergebene Ressource von der Methode selbst geschlossen wird oder nicht.
  • @ThreadSafe bzw. @NotThreadSafe – Diese beiden sind, so nehme ich an, dem Namen nach selbsterklärend. Ein StringBuilder wäre dabei @NotThreadSafe, während der StringBuffer als @ThreadSafe annotiert werden könnte.

Fazit

Die Situation bei den Creational Pattern zeigt, dass es beim Einsatz von Design Pattern leicht zu Missverständnissen kommen kann. Die Erwartung, dass durch die Verwendung von Pattern Code von einem anderen Entwickler einfacher verstanden wird, ist also nicht unbedingt automatisch erfüllt. Die Erstellung eines eigenen Pattern-Kataloges ist dabei die empfohlene Maßnahme zur Vermeidung der typischen Missdeutungen. Darüber hinaus wird das Thema Design Pattern im Team automatisch etabliert, im Zuge der Erstellung des Katalogs.

Verwandte Themen:

Geschrieben von
Herbert Dowalil
Herbert Dowalil
Herbert Dowalil ist seit Jahren als Softwarearchitekt, Designer und Coach tätig.
Kommentare
  1. TestP2017-02-22 11:46:27

    Der Artikel ist zwar echt interessant aber "Interface extenden" oder "somit erst zur Runtime" wirkt meiner Meinung nach sehr fehlplatziert so ein "Denglisch".

    Grüße

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.