dark

Builder-Pattern auf Steroiden

Avatar

Das Buch der GangOfFour gehört bei so ziemlich jedem Ausbildungszweig der Informatik mit zu der Grundlagenlektüre. Dort werden die Basispattern beschrieben und gruppiert, so dass man einen guten Start in das Thema DesignPattern bekommt. Aber wie sieht es dann später in der Verwendung aus? Hier werden wir das eine oder andere Pattern genauer unter die Lupe nehmen und erweitern.

Das Builder-Pattern erfreut sich derzeitig immer größerer Beliebtheit, ermöglicht es einem doch die Konstruktion eines fluent-API. Angenehm ist es ebenfalls, dass eine IDE dieses Pattern recht einfach generieren kann. Dem Einsatz steht nun also nichts mehr im Wege, aber… Wie sieht es mit der Verwendung im täglichen Leben aus?

Wer möchte, kann sich auch dazu mein Video auf Youtube ansehen: https://youtu.be/eaI1L0viE3k

Die Grundversion vom Builder-Pattern

Beginnen wir mit dem Basispattern, dem Grundstock, mit dem wir sicherlich schon alle unsere Erfahrungen gesammelt haben. Als Beispiel nehme ich hier eine Klasse Car mit den Attributen Engine und List<Wheel>. Sicherlich ist die Beschreibung eines Autos hiermit nicht sehr präzise, jedoch reicht es, um einige Eigenarten des Builder-Pattern recht deutlich darzustellen.

public class Car {
	private Engine engine;
	private List&lt;Wheel&gt; wheelList;
	//SNIPP
}

An dieser Stelle lasse ich die get– und set-Methoden im Listing weg. Wenn man nun hierzu einen Builder generiert, bekommt man in etwa das Folgende:

public static final class Builder {
	private Engine engine;
    private List wheelList;
	
    private Builder() { }
	    public Builder withEngine(Engine engine) {
		this.engine = engine;
		return this;
	}
    public Builder withWheelList(List wheelList) {
	    this.wheelList = wheelList;
	    return this;
    }
    public Car build() {
        return new Car(this);
    }
}

Hier ist der Builder als innere statische Klasse realisiert. Ebenfalls ist der Konstruktor der Klasse Car modifiziert worden.

private Car(Builder builder) {
	setEngine(builder.engine);
	wheelList = builder.wheelList;
}

Zum einen hat ein Wechsel von public auf private stattgefunden und zum anderen wurde eine Instanz des Builders als Methoden-Parameter hinzugefügt.

Car car = Car.newBuilder()
	.withEngine(engine)
	.withWheelList(wheels)

Ein Beispiel – Das Auto

Wenn man nun mit dem Builder-Pattern arbeitet, gelangt man an den Punkt, an dem man komplexe Objekte aufbauen muss. Erweitern wir nun unser Beispiel, indem wir uns die restlichen Attribute von der Klasse Car ansehen.

public class Car {
	private Engine engine;
	private List 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 dieser Klassen 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);
	}
}

Wie aber verhält es sich, wenn man eine Instanz der Klasse Car erzeugen möchte? Für jedes komplexe Attribut von Car werden wir mittels Builder eine Instanz erzeugen. Der daraus resultierende Quelltext ist recht umfangreich, eine Reduktion der Menge als auch Komplexität hat auf den ersten Blick nicht stattgefunden.

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 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 alle Potentiale heben zu können, müssen wir den Quelltext homogen gestalten. So sind wir in der Lage, Muster einfacher zu erkennen. In unserem Beispiel ist es die Erzeugung der List<Wheel> in einen Builder auszulagern, einem WheelListBuilder.

public class WheelListBuilder {
    public static WheelListBuilder newBuilder(){
        return new WheelListBuilder();
    }
    private WheelListBuilder() {}
    private List wheelList;
    public WheelListBuilder withNewList(){
        this.wheelList = new ArrayList&lt;&gt;();
        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 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 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 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 Liste 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();

Der NestedBuilder

Bisher wurden die Builder von Hand einzeln modifiziert. Dieses kann man aber recht einfach generisch implementieren, da es sich hier lediglich um einen Baum von Buildern handelt. 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 bei den meisten Generatoren für Builder so zu sein scheint, ist hier keine manuelle Anpassung notwendig. Die Methode done() setzt das Ergebnis seiner eigenen build() Methode bei seinem Vater. Der Aufruf erfolgt mittels Reflection. Hiermit kennt ein Vater die Instanz des Kindes. An dieser Stelle nehme ich im Moment an, dass der Name des Attributes gleich dem Klassennamen ist. Wir werden später sehen, wie man das mit anders lautenden Attributnamen realisieren kann. Die Methode withParentBuilder ermöglicht es dem Vater sich bei seinem Kind bekannt zu geben. Hiermit haben wir nun die bidirektionale Verbindung.

public abstract class NestedBuilder<T, V> {
    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;
    public > P withParentBuilder(T parent) {
        this.parent = parent;
        return (P) this;
    }
}

Nun können einem Vater 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;
        //snipp.....
        // 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;
    //snipp.....
    public static final class Builder extends NestedBuilder&lt;Parent.Builder, KidA&gt; {
    //snipp.....
    }
}

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);
    }
}

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);
    }
}

Viel Spaß beim Ausprobieren. Wer möchte, kann sich dazu auch noch mein Video auf Youtube ansehen: https://youtu.be/eaI1L0viE3k

Cheers

Sven

Sven Ruppert entwickelt seit 1996 in Java an Industrieprojekten. Er war über 15 Jahre als Berater weltweit in Branchen wie Automobil, Raumfahrt, Versicherungen, Bankwesen, UNO und WorldBank tätig. Sven ist Groundbreaker Ambassador (ehem. Oracle Developer Champion) und arbeitet als Developer-Advocate für JFrog. Er spricht regelmäßig auf Konferenzen weltweit und schreibt für IT-Zeitschriften sowie Tech-Portalen. Neben seinem Hauptthema DevSecOps und den Evergreen-Themen Core-Java und Kotlin arbeitet er an Mutationstests von Web-Apps und Distributed UnitTesting.
Total
0
Shares
Previous Post
svenruppert.com

Delegation versus Inheritance in grafischen Oberflächen

Next Post

Die typische Lebenslinie einer Sicherheitslücke

Related Posts