Cascaded Builder Pattern in Java

2 Kommentare

Wenn man mit dem Builder Pattern arbeitet, gelangt man an den Punkt, an dem man komplexe Objekte aufbauen muss. Nehmen wir nun an, dass wir ein Auto erzeugen möchten. Dieses besteht aus den Attributen Motor, Maschine und einer Anzahl Räder. Hierfür verwenden wir nun das nachfolgende Klassenmodell.

public class Car {
    private Engine engine;
    private List<Wheel> wheelList;
}
 
public class Engine {
    private int power;
    private int type;
}
 
public class Wheel {
    private int size;
    private int type;
    private int colour;
}

Nun kann man für jede Klasse einen entsprechenden Builder generieren lassen. Wenn man sich dabei an das Basispattern hält, sieht das für die Klasse Wheel in etwa so aus:

public static final class Builder {
        private int size;
        private int type;
        private int colour;
 
        private Builder() {}
 
        public Builder withSize(int size) {
            this.size = size;
            return this;
        }
 
        public Builder withType(int type) {
            this.type = type;
            return this;
        }
 
        public Builder withColour(int colour) {
            this.colour = colour;
            return this;
        }
 
        public Wheel build() {
            return new Wheel(this);
        }
    }

Der Builder ist als inner static class realisiert und vollzieht demnach die Änderungen in der Klasse Wheel nach, damit nur noch über den Builder gegangen werden kann, um eine Instanz zu erzeugen. Natürlich habe ich hierbei die Möglichkeiten via Reflection ausgelassen.

public class Wheel {
 
    private int size;
    private int type;
    private int colour;
 
    private Wheel(Builder builder) {
        setSize(builder.size);
        setType(builder.type);
        setColour(builder.colour);
    }
 
    public static Builder newBuilder() {
        return new Builder();
    }
 
    public static Builder newBuilder(Wheel copy) {
        Builder builder = new Builder();
        builder.size = copy.size;
        builder.type = copy.type;
        builder.colour = copy.colour;
        return builder;
    }
...}

Wie aber sieht es nun aus, wenn man eine Instanz der Klasse Car erzeugen möchte? Hier kommen wir zu dem Punkt, dass wir die Instanz der Klasse Wheel der Instanz der Klasse Car hinzufügen wollen.

public class Main {
  public static void main(String[] args) {
 
    Engine engine = Engine.newBuilder().withPower(100).withType(5).build();
 
    Wheel wheel1 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel2 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel3 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
 
    List<Wheel> wheels = new ArrayList<>();
    wheels.add(wheel1);
    wheels.add(wheel2);
    wheels.add(wheel3);
 
    Car car = Car.newBuilder()
                 .withEngine(engine)
                 .withWheelList(wheels)
                 .build();
 
    System.out.println("car = " + car);
  }
}

Dieser Quelltext ist nicht sonderlich schön und auf keinen Fall kompakt. Wie also kann man hier das Builder Pattern anpassen, damit man auf der einen Seite möglichst wenig von dem Builder selbst von Hand schreiben muss und auf der anderen Seite bei der Verwendung mehr Komfort bekommt?

WheelListBuilder

Gehen wir zuerst einen kleinen Umweg. Um zum Beispiel sicherzustellen, dass man einem Auto nur vier Räder hinzufügen kann, kann man z. B. einen WheelListBuilder erzeugen. Hier kann man z. B. in der Methode build() überprüfen, ob vier Instanzen der Klasse Wheel vorhanden sind.

public class WheelListBuilder {
 
    public static WheelListBuilder newBuilder(){
      return new WheelListBuilder();
    }
 
    private WheelListBuilder() {}
 
    private List<Wheel> wheelList;
 
    public WheelListBuilder withNewList(){
        this.wheelList = new ArrayList<>();
        return this;
    }
    public WheelListBuilder withList(List wheelList){
        this.wheelList = wheelList;
        return this;
    }
 
    public WheelListBuilder addWheel(Wheel wheel){
        this.wheelList.add(wheel);
        return this;
    }
 
    public List<Wheel> build(){
        //test if there are 4 instances....
        return this.wheelList;
    }
 
}

Nun sieht unser Beispiel von vorhin wie folgt aus:

public class Main {
  public static void main(String[] args) {
 
    Engine engine = Engine.newBuilder().withPower(100).withType(5).build();
 
    Wheel wheel1 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel2 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
    Wheel wheel3 = Wheel.newBuilder().withType(2).withColour(3).withSize(4).build();
 
//        List<Wheel> wheels = new ArrayList<>();
//        wheels.add(wheel1);
//        wheels.add(wheel2);
//        wheels.add(wheel3);
 
    List<Wheel> wheelList = WheelListBuilder.newBuilder()
        .withNewList()
        .addWheel(wheel1)
        .addWheel(wheel2)
        .addWheel(wheel3)
        .build();//more robust if you add tests at build()
 
    Car car = Car.newBuilder()
        .withEngine(engine)
        .withWheelList(wheelList)
        .build();
 
    System.out.println("car = " + car);
  }
}

Als nächstes verbinden wir den Builder der Klasse Wheel und die Klasse WheelListBuilder. Das Ziel ist es, ein Fluent API zu erhalten, damit wir nicht die Instanzen der Klasse Wheel einzeln erzeugen und dann diese mit der Methode addWheel(Wheel w) dem WheelListBuilder hinzufügen müssen. Es soll dann für den Entwickler in der Verwendung wie folgt aussehen:

List<Wheel> wheels = wheelListBuilder
    .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
    .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
    .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
    .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
    .build();

Was hier also passiert, ist folgendes: Sobald die Methode addWheel() aufgerufen wird, soll eine neue Instanz der Klasse WheelBuilder zurückgegeben werden. Die Methode addWheelToList() erzeugt die Instanz der Klasse Wheel und fügt sie der List hinzu.
Um das zu erreichen, muss man die beiden beteiligten Builder modifizieren. Auf der Seite des WheelBuilder kommt die Methode addWheelToList() hinzu. Diese fügt die Instanz der Klasse Wheel dem WheelListBuilder hinzu und liefert die Instanz der Klasse WheelListBuilder zurück.

private WheelListBuilder wheelListBuilder;
 
public WheelListBuilder addWheelToList(){
  this.wheelListBuilder.addWheel(this.build());
  return this.wheelListBuilder;
}

Auf der Seite der Klasse WheelListBuilder wird lediglich die Methode addWheel() hinzugefügt.

public Wheel.Builder addWheel() {
    Wheel.Builder builder = Wheel.newBuilder();
    builder.withWheelListBuilder(this);
    return builder;
  }

Wenn wir nun dieses auf die anderen Builder übertragen, kommen wir zu einem recht ansehnlichen Ergebnis:

Car car = Car.newBuilder()
          .addEngine().withPower(100).withType(5).done()
          .addWheels()
            .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
            .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
            .addWheel().withType(1).withSize(2).withColour(2).addWheelToList()
          .done()
          .build();

NestedBuilder

Bisher wurden die Builder von Hand einzeln modifiziert. Dieses kann man aber recht einfach generisch implementieren. Es handelt sich lediglich um einen Baum von Buildern. Jeder Builder kennt demnach seine Kinder und seinen Vater. Die dafür notwendigen Implementierungen sind in der Klasse NestedBuilder zu finden. Hierbei wird angenommen, dass die Methoden zum Setzen von Attributen immer mit with beginnen. Da dies aber bei den meisten Generatoren für Builder so zu sein scheint, ist hier keine manuelle Anpassung notwendig.

public abstract class NestedBuilder<T, V> {
  /**
   * To get the parent builder
   *
   * @return T the instance of the parent builder
   */
  public T done() {
    Class<?> parentClass = parent.getClass();
    try {
      V build = this.build();
      String methodname = "with" + build.getClass().getSimpleName();
      Method method = parentClass.getDeclaredMethod(methodname, build.getClass());
      method.invoke(parent, build);
    } catch (NoSuchMethodException 
            | IllegalAccessException 
            | InvocationTargetException e) {
      e.printStackTrace();
    }
    return parent;
  }
 
  public abstract V build();
 
  protected T parent;
 
  /**
   * @param parent
   * @return
   */
  public <P extends NestedBuilder<T, V>> P withParentBuilder(T parent) {
    this.parent = parent;
    return (P) this;
  }
}

Nun können einem Parent die spezifischen Methoden für die Verbindungen zu den Kindern hinzugefügt werden. Ein Ableiten von NestedBuilder ist nicht erforderlich.

public class Parent {
 
  private KidA kidA;
  private KidB kidB;
 
  //snipp.....
 
  public static final class Builder {
    private KidA kidA;
    private KidB kidB;
 
    private Builder() {
    }
 
    public Builder withKidA(KidA kidA) {
      this.kidA = kidA;
      return this;
    }
 
    public Builder withKidB(KidB kidB) {
      this.kidB = kidB;
      return this;
    }
 
    // to add manually
    private KidA.Builder builderKidA = KidA.newBuilder().withParentBuilder(this);
    private KidB.Builder builderKidB = KidB.newBuilder().withParentBuilder(this);
 
    public KidA.Builder addKidA() {
      return this.builderKidA;
    }
 
    public KidB.Builder addKidB() {
      return this.builderKidB;
    }
    //---------
 
    public Parent build() {
      return new Parent(this);
    }
  }
}

Und bei den Kindern sieht es wie folgt aus: Hier muss lediglich von NestedBuilder abgeleitet werden.

public class KidA {
 
  private String note;
 
  private KidA(Builder builder) {
    note = builder.note;
  }
 
  public static Builder newBuilder() {
    return new Builder();
  }
 
  public static final class Builder extends NestedBuilder<Parent.Builder, KidA> {
    private String note;
 
    private Builder() {
    }
 
    public Builder withNote(String note) {
      this.note = note;
      return this;
    }
 
    public KidA build() {
      return new KidA(this);
    }
 
  }
}

Die Verwendung ist dann, wie in dem vorherigen Beispiel gezeigt, sehr kompakt.

public class Main {
  public static void main(String[] args) {
    Parent build = Parent.newBuilder()
        .addKidA().withNote("A").done()
        .addKidB().withNote("B").done()
        .build();
    System.out.println("build = " + build);
  }
}

Fazit

Natürlich ist auch eine beliebige Kombination möglich. Das bedeutet, dass ein Proxy Vater und Kind gleichzeitig sein kann. Dem Aufbau komplexer Strukturen steht nun nichts mehr im Wege.

public class Main {
  public static void main(String[] args) {
    Parent build = Parent.newBuilder()
        .addKidA().withNote("A")
                  .addKidB().withNote("B").done()
        .done()
        .build();
    System.out.println("build = " + build);
  }
}

Kommentare

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.