Beiträge Design Pattern - Builder
Post
Cancel

Design Pattern - Builder

Heute geht es um das Builder Pattern bzw das Entwurfsmuster Erbauer.

Der Beispielcode kann in dem GitHub Repository seism0saurus/design-pattern-builder eingesehen werden. Ich habe ein Package für den Quellcode ohne das Entwurfsmuster und ein Package für den Code mit dem Entwurfsmuster erstellt. Beide enthalten eine Example Klasse, deren Main Methode ausgeführt werden kann. Es gibt insgesamt zwei Beispiele in diesem Artikel und in dem Repository. Denn oft wird nicht das gesamte Entwurfsmuster umgesetzt, sondern nur der Builder für bestimmte Objekte mit einem Fluent Interface implementiert. Auch dadurch wird die Erzeugung des Objektes von dem Objekt selbst entkoppelt. Allerdings muss der Client wissen, welches Objekt er haben möchte und den Builder aufrufen. Das ist oft in Ordnung. Aber manchmal möchte man mehr Flexibilität und da kommt das komplette Builder Pattern inklusive Director zum Einsatz.

Zweck

Ein Erbauer kapselt die Erzeugung eines Objektes, für das es viele verschiedene Konfigurationsmöglichkeiten gibt. Dabei ist sichergestellt, dass ein in sich konsistentes Objekt entsteht und nicht für alle Felder Setter veröffentlicht werden müssen. Durch einen Director ist es möglich, zur Laufzeit einen Erbauer zu übergeben und der Director nutzt diesen, um die benötigten Objekte zu erzeugen.

Vereinfachtes Beispiel Pizza

Als Beispiel für die vereinfachte Variante, die lediglich aus einem Builder für eine Klasse besteht, betrachten wir eine Pizza. Eine Pizza besteht aus einem Pizzaboden, dann kommt Soße und eine Käsesorte obendrauf. Je nach Geschmack noch Gemüse, Fleisch und Extras. VeganerInnen lassen den Käse weg oder ersetzen ihn mit Hefeschmelz.

In der vereinfachten Variante entkoppeln wir lediglich die Erzeugung eines Objekts von dem Objekt selbst. Durch ein fluent Interface wird die Erzeugung einfacher und übersichtlicher.

Vorher oder Antipatern

Damit ich alle Möglichkeiten bei der Pizzazusammenstellung habe, baue ich mir einen Konstruktor, in den ich Boden, Käse, Gemüse etc. direkt übergebe. Methoden und Konstruktoren mit vielen Parametern sind schlecht zu lesen uns zu benutzen. War der fünfte String Parameter jetzt der Käse oder doch das Gemüse? Jedes Mal nachzusehen kostet Zeit und führt zu unnötigen Fehlern. Die Konstruktoren hintereinander zu hängen, um dem Client auch Konstruktoren mit weniger Parameter anzubieten, nennt man übrigens Telescoping Constructor Pattern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Pizza {

  private final String base;
  private final String cheese;
  private final String vegetable;
  private final String meat;
  private final String extra;

  public Pizza(String base) {
    this(base, null);
  }

  public Pizza(String base, String cheese) {
    this(base, cheese, null);
  }

  public Pizza(String base, String cheese, String vegetable) {
    this(base, cheese, vegetable, null);
  }

  public Pizza(String base, String cheese, String vegetable, String meat) {
    this(base, cheese, vegetable, meat, null);
  }

  public Pizza(String base, String cheese, String vegetable, String meat, String extra) {
    this.base = base;
    this.cheese = cheese;
    this.vegetable = vegetable;
    this.meat = meat;
    this.extra = extra;
  }
}

Für den Client sind die Konstruktoren nicht sehr schön. Zum einen muss er null Werte übergeben, um optionale Felder leer zu lassen. Zum anderen ist auf den ersten Blick nicht ersichtlich, für was welcher Parameter steht. Wenn ich bei der Käsepizza statt einem zweiten Käse, lieber Knoblauch haben möchte, ändere ich dann den zweiten oder den fünften Parameter?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Example {

  public static void main(String[] args) {
    Pizza margherita = new Pizza("italian", "mozzarella", "tomato", null, "basil");
    System.out.println(margherita);

    Pizza cheese = new Pizza("italian", "gauda", null, null, "cheddar");
    System.out.println(cheese);

    Pizza salami = new Pizza("italian", "emmental", null, "salami");
    System.out.println(salami);
  }
}

Die Ausgabe unserer Pizzabestellung sieht übrigens so aus, da ich noch die toString() Methode überschrieben habe.

1
2
3
Pizza with italian base and with molten mozzarella and with sliced tomato and with extra basil
Pizza with italian base and with molten gauda and with extra cheddar
Pizza with italian base and with molten emmental and with bits of salami

Nachher oder das Erbauer-Muster

Statt den Client mit vielen und langen Konstruktoren zu quälen, löschen wir alle bis auf einen Konstruktor. Dieser wird private und hat alle nötigen Übergabeparameter.

1
2
3
4
5
6
7
    private Pizza(String base,String cheese,String vegetable,String meat,String extra){
  this.base=base;
  this.cheese=cheese;
  this.vegetable=vegetable;
  this.meat=meat;
  this.extra=extra;
  }

Damit von außerhalb der Pizzaklasse überhaupt noch eine Pizza erzeugt werden kann, bauen wir eine statische Unterklasse, den Builder. Da sich der Builder innerhalb der Pizzaklasse befindet, kann er den Konstruktor noch aufrufen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static class Builder {
  private String base;
  private String cheese;
  private String vegetable;
  private String meat;
  private String extra;

  public Builder base(String base) {
    this.base = base;
    return this;
  }

  public Builder cheese(String cheese) {
    this.cheese = cheese;
    return this;
  }

  public Builder vegetable(String vegetable) {
    this.vegetable = vegetable;
    return this;
  }

  public Builder meat(String meat) {
    this.meat = meat;
    return this;
  }

  public Builder extra(String extra) {
    this.extra = extra;
    return this;
  }

  public Pizza build() {
    return new Pizza(base, cheese, vegetable, meat, extra);
  }
}

Komplettes Beispiel Drei Gänge Menüs

Der gezeigte Builder bei der Pizza ist eine vereinfachte Version des kompletten Builderpaterns. Das komplette Entwurfsmuster enthält nicht nur eine Klasse und einen Builder dafür. Vielmehr gibt es ein Builder Interface und je einen konkreten Builder für die erzeugten Produkte. Dazu gibt es dann noch einen Director, der den korrekten Aufruf der Builder sicherstellt und je nach übergebenen konkreten Builder ein passendes Objekt erzeugt. Hier ähnelt das Entwurfsmuster der abstrakten Fabrkik. Daher habe den Code aus dem Artikel als Gegenbeispiel genommen, um die Unterschiede herauszuarbeiten.

Der Director weiß nach welchem Schema die Produkte gebaut werden müssen. Dazu bekommt er einen konkreten Builder übergeben und greift über dessen Interface auf den Builder zu und baut damit einzelne Teile und fügt sie zu dem Product zusammen.

Im Gegensatz zur Factory, entscheidet der Client über den übergebenen Builder, wie das fertige Produkt aussieht.

Vorher oder die Abstract Factory

Durch die abstrakte Fabrik kann der Client eine Küche wählen und ihm wird ein passendes Menü erzeugt.

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[]args){
  MenuFactory menuFactory=AbstractMenuFactory.getFactory(Flavour.KOREAN);

  FirstCourse firstCourse=menuFactory.getFirstCourse();
  MainCourse mainCourse=menuFactory.getMainCourse();
  Dessert dessert=menuFactory.getDesert();

  System.out.println(firstCourse);
  System.out.println(mainCourse);
  System.out.println(dessert);
  }

Der Client muss sich nicht um die Erzeugung des Menüs kümmern, hat aber auch keinen Einfluss auf die einzelnen Gänge.

Nachher oder der Erbauer

Der Code lässt sich so umbauen, dass der statt der abstrakten Fabrik einen Erbauer nutzt.

Das Interface für das Rezept sieht für unsere drei Gänge Menüs wie folgt aus. Das Rezept ist in unserem Fall der Erbauer aus dem Entwurfsmuster.

1
2
3
4
5
6
7
public interface Recipe {
  FirstCourse firstCourse();

  MainCourse mainCourse();

  Dessert dessert();
}

Das Rezept Interface wird nun von den konkreten Rezepten implementiert. Hier das Beispiel für das Rezept eines koreanischen Menüs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class KoreanRecipe implements Recipe {
  @Override
  public FirstCourse firstCourse() {
    return new KoreanSpringRoll();
  }

  @Override
  public MainCourse mainCourse() {
    return new TofuWithVegetables();
  }

  @Override
  public Dessert dessert() {
    return new HoneyCakeFromRice();
  }
}

Neben diesem gibt es noch weitere konkrete Implementierungen und der Client kann eigene Rezepte implementieren und dem Director übergeben. Der Director in unserem Beispiel ist der Koch. Der Koch weiß, wie aus dem Rezept ein Menü gebaut oder gekocht werden kann.

1
2
3
4
5
6
7
8
9
10
public class Chef {

  public Menu cook(Recipe recipe) {
    return menu(recipe.firstCourse(), recipe.mainCourse(), recipe.dessert());
  }

  private Menu menu(FirstCourse firstCourse, MainCourse mainCourse, Dessert dessert) {
    return new Menu(firstCourse, mainCourse, dessert);
  }
}

Der Erbauer erzeugt am Ende ein Product. Unser Koch kocht mithilfe der Rezepte Menüs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Menu {
  private final FirstCourse firstCourse;
  private final MainCourse mainCourse;
  private final Dessert dessert;

  public Menu(FirstCourse firstCourse, MainCourse mainCourse, Dessert dessert) {
    this.firstCourse = firstCourse;
    this.mainCourse = mainCourse;
    this.dessert = dessert;
  }

  @Override
  public String toString() {
    return "First course: " + firstCourse +
      "\nMain course: " + mainCourse +
      "\nDessert: " + dessert;
  }
}

Die konkrete Implementierung der Gänge ist nicht so wichtig. Es sind einfach Bestandteile des fertigen Menüs.

Nun haben wir alle Komponenten des Entwurfsmusters beisammen und können einen Client bauen.

1
2
3
4
5
6
7
8
public class Example {

  public static void main(String[] args) {
    Chef chef = new Chef();
    Menu menu = chef.cook(new KoreanRecipe());
    System.out.println(menu);
  }
}

Dem Koch wird ein Rezept für das koreanische Menü übergeben und er kocht daraus ebendieses. Der Client muss sich nicht darum kümmern, wie das Menü gekocht wird. Im Gegensatz zur abstrakten Fabrik haben wir aber mehr Flexibilität. Wir können dem Koch auch sagen, dass wir gerne ein spezielles Menü hätten.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Example {

  public static void main(String[] args) {
    Chef chef = new Chef();

    Recipe customRecipe = new Recipe() {
      @Override
      public FirstCourse firstCourse() {
        return new TomatoMozzarellaPesto();
      }

      @Override
      public MainCourse mainCourse() {
        return new Enchiladas();
      }

      @Override
      public Dessert dessert() {
        return new ChocolateLavaCake();
      }
    };

    menu = chef.cook(customRecipe);
    System.out.println(menu);
  }
}

Auch hier ist dem Client egal, wie der Koch das Menü zubereitet. Er muss ihm nur mitteilen, was er gerne hätte, und der Koch kümmert sich darum, dass daraus ein Menü wird.

Vorteile

Das Erzeugen des Objekts ist von dem Objekt selbst entkoppelt und kann einfacher gestaltet werden. Im Gegensatz zur abstrakten Fabrik hat der Client mehr Flexibilität bei der Zusammenstellung der Objekte.

Nachteile

Die Flexibilität und Entkopplung des Objekts und dessen Erzeugung erhöhen die Komplexität des Codes.

Zusammenfassung

Oft kann schon der Builder ohne Director die Verwendung des Codes deutlich vereinfachen, wie wir bei der Pizza gesehen haben. Umständliche Konstruktoren können vermieden werden und der Client kann sich einfach ein passendes und in sich konsistentes Objekt bauen. Mit dem vollständigen Builder inkl. Director kann das Entwurfsmuster verwendet werden, um ein hohe Flexibilität bei der Erzeugung von komplexen Objekten und Objektgruppen zu bewahren, ohne den Code für den Client zu kompliziert zu gestalten.

Bis bald,

seism0saurus

Dieser Blogbeitrag wurde vom Autor unter der CC BY 4.0 lizenziert.