Das Builder Pattern
Heute zeige ich euch, wie ihr mit dem Builder Pattern und Hilfsklassen komplexen und trotzdem gute lesbaren Code schreiben könnt.
Heute stelle ich euch das Builder Pattern vor. Das Builder Pattern ist ein Entwurfsmuster, das Konstruktionssicherheit bietet und die Lesbarkeit Ihres Codes erhöht. Ziel ist, dass ich ein Objekt nicht mit den üblichen Konstruktoren erstelle, sondern über eine Hilfsklasse.
Wofür sind Hilfsklassen gut?
Nehmen wir mal an, ich habe eine Klasse Auto. Nun wissen wir auch, wie viele mögliche Attribute ein Auto haben kann. Von Geschwindigkeit, PS, KW, Türen und so weiter und so fort. Nehmen wir weiter an, dass ich beim Erstellen eines Autos alle Attribute setzen muss. Nun habe ich zwei Möglichkeiten.
public class CarWithoutBuilder {
private int kw;
private int doors;
private int wheels;
private String type;
private String constructor;
public CarWithoutBuilder(){}
public CarWithoutBuilder(int kw, int doors,
int wheels, String type, String constructor) {
this.kw = kw;
this.doors = doors;
this.wheels = wheels;
this.type = type;
this.constructor = constructor;
}
public int getKw() {
return kw;
}
public void setKw(int kw) {
this.kw = kw;
}
public int getDoors() {
return doors;
}
public void setDoors(int doors) {
this.doors = doors;
}
public int getWheels() {
return wheels;
}
public void setWheels(int wheels) {
this.wheels = wheels;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getConstructor() {
return constructor;
}
public void setConstructor(String constructor) {
this.constructor = constructor;
}
}
Möglichkeit 1, ich erstelle eine Instanz, von der erstellten-Auto Klasse und befülle die Attribute mit Werten. Oder Möglichkeit 2, ich erstelle einen passenden Konstruktor und übergebe die notwendigen Parameter.
In der Klasse CarWithoutBuilder ist beides umgesetzt. Alle Parameter haben einen Getter und einen Setter. Ausserdem habe ich einen Konstruktor mit einem Parameter für jedes Attribut in der Klasse erstellt.
Ein mögliches Erstellen würde jetzt wie folgt aussehen:
CarWithoutBuilder carAlternative = new CarWithoutBuilder();
carAlternative.setKw(55);
carAlternative.setDoors(5);
carAlternative.setWheels(4);
carAlternative.setType("A3");
carAlternative.setConstructor("Audi");
In diesem Bild erstelle ich eine Instanz und setze alle Attribute über die Setter-Funktionen.
Dies sieht hier noch recht übersichtlich aus. Nun stellen Sie sich das für eine Klasse mit 20 oder mehr Attributen vor. Richtig, es wird eine «Wall of Text» und das ist dann weder schön noch angenehm lesbar. Wenn nur schon das Erstellen einer Funktion 20 Zeilen einnimmt, schadet dies ganz klar der Lesbarkeit.
Variante mit Konstruktor
Schauen wir uns nun den Aufruf mit dem Konstruktor an. Der Konstruktor hat in unserem Fall fünf Parameter.
CarWithoutBuilder carAlternativeByConstructor =
new CarWithoutBuilder(55,5,4,"A3","Audi");
CarWithoutBuilder carAlternativeByConstructor2 =
new CarWithoutBuilder(55,5,4,null,null);
Ich erstelle zuerst zwei Car-Instanzen. In der zweiten sind Type und Constructor noch null, da es sich um einen Prototypen handelt.
Was fällt uns hier auf? Wir haben zum einen nur noch fünf Parameter, was gerade noch erträglich, aber auf keinen Fall schön ist. Auch fällt auf, dass man bei den drei Zahlen nicht zuordnen kann, welche Zahl welchen Parameter befüllt. Nun stellen wir uns vor, dass wir 20 Parameter des Autos befüllen müssen. Davon abgesehen das 20 Parameter in einer Funktion zu vermeiden sind, könnte kein Entwickler mehr anhand des Aufrufs bestimmen, welche Parameter übergeben worden sind.
Was kann man also dagegen tun? Das Builder Pattern baut darauf auf, dass man zum Erstellen einer Instanz eine eigene Klasse hat. Diese dient nur dem Erstellen des Objekt. Bevor ich weiter ins Detail gehe, zeige ich euch mal die zwei neuen Klassen, welche ich erstellt habe.
Zuerst die Car Klasse, die wir erstellen wollen.
public class Car {
private int kw;
private int doors;
private int wheels;
private String type;
private String constructor;
public Car(CarBuilder builder){
this.kw = builder.getKw();
this.doors = builder.getDoors();
this.wheels = builder.getWheels();
this.type = builder.getType();
this.constructor = builder.getConstructor();
}
public int getKw() {
return kw;
}
public void setKw(int kw) {
this.kw = kw;
}
public int getDoors() {
return doors;
}
public void setDoors(int doors) {
this.doors = doors;
}
public int getWheels() {
return wheels;
}
public void setWheels(int wheels) {
this.wheels = wheels;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getConstructor() {
return constructor;
}
public void setConstructor(String constructor) {
this.constructor = constructor;
}
}
Ich habe hier einen public-Konstruktor, der als Parameter einen CarBuilder erwartet. Dieser CarBuilder sieht wie folgt aus:
public class CarBuilder {
private int kw;
private int doors;
private int wheels;
private String type;
private String constructor;
public CarBuilder withKw(int kw){
this.kw = kw;
return this;
}
public CarBuilder withDoors(int doors){
this.doors = doors;
return this;
}
public CarBuilder withWheels(int wheels){
this.wheels = wheels;
return this;
}
public CarBuilder withType(String type){
this.type = type;
return this;
}
public CarBuilder withConstructor(String constructor){
this.constructor = constructor;
return this;
}
public int getKw() {
return kw;
}
public int getDoors() {
return doors;
}
public int getWheels() {
return wheels;
}
public String getType() {
return type;
}
public String getConstructor() {
return constructor;
}
public Car create(){
return new Car(this);
}
}
Ich sehe hier ähnliche Parameter wie in der Car Klasse. Dafür sind die Funktionen anders aufgebaut. Jede Funktion zum Setzen eines Wertes gibt die Instanz selbst zurück.
Die Builder-Klasse
Schaue ich mir nun die Erstellung eines Objekts mit der Builder-Klasse an, so sieht dies wie folgt aus:
Car car = new CarBuilder().withKw(150).withDoors(5).withWheels(4).withType("A3")
.withConstructor("Auti").create();
Durch die Form des Builders kann ich alle Funktionen direkt hintereinander ausführen, da ich immer wieder das Ursprungsobjekt zurückbekomme. So kann ich alle Attribute befüllen, ohne viel Text zu schreiben. Auch habe ich nur einzelne Parameter als Übergabewert. Muss ein Attribut bei der Initialisierung null sein, so lasse ich es im Aufruf einfach weg. So kann ich optionale Parametern optimal berücksichtigen.
Nehmen wir nun an, wir erweitern unsere Car-Klasse in einer späteren Version. Wenn ich einen Konstruktor zum Erstellen verwendet habe, muss ich nun den Konstruktor in der neuen Version überladen. Das heisst, einen zusätzlichen Konstruktor erstellen, welcher den zusätzlichen neuen Attributwert enthält. Alternativ würde noch die Möglichkeit bestehen, alle Aufrufe zu bearbeiten. Sobald aber der eigene Code in anderen Projekten verwendet wird, ist auch dies schwierig umzusetzen.
Das Builder Pattern schafft Abhilfe
Schauen wir uns dieses Problem im Builder Pattern an. Ein neues Attribut wird hinzugefügt. Nehmen wir an, ich füge highSpeed als neues Attribut hinzu. Ich lege also in der Builder Klasse eine Funktion withHighspeed an.
Nun habe ich die Wahl, ob ich alte Aufrufe anpasse oder den default-Wert stehen lasse. Bei neuen Implementierungen steht mir jedoch die neue Funktion withHighspeed zur Verfügung.
Die create-Funktion
Jeder Builder hat eine create-Funktion. Diese ruft den Konstruktor der zu erstellenden Klasse auf und übergibt sich selbst. Im Konstruktor der eigentlichen Klasse hat man also nun den Builder mit allen Elementen zur Verfügung und kann das erstellte Object übergeben.
Das war das Builder Pattern.
Es geht noch einfacher …
Ich möchte hier jedoch noch eine Alternative von Joshua Bloch vorstellen. Diese hat er in seinem Buch Effective Java beschrieben. Er weicht von dem Builder Pattern ab, ist aus meiner Sicht jedoch verständlicher und senkt die Komplexität.
public class CarAlternative {
private int kw;
private int doors;
private int wheels;
private String type;
private String constructor;
private CarAlternative(){}
private CarAlternative(Builder builder){
this.kw = builder.kw;
this.doors = builder.doors;
this.wheels = builder.wheels;
this.type = builder.type;
this.constructor = builder.constructor;
}
public int getKw() {
return kw;
}
public void setKw(int kw) {
this.kw = kw;
}
public int getDoors() {
return doors;
}
public void setDoors(int doors) {
this.doors = doors;
}
public int getWheels() {
return wheels;
}
public void setWheels(int wheels) {
this.wheels = wheels;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getConstructor() {
return constructor;
}
public void setConstructor(String constructor) {
this.constructor = constructor;
}
public static class Builder{
private int kw;
private int doors;
private int wheels;
private String type;
private String constructor;
public Builder(){}
public Builder withKw(int kw){
this.kw = kw;
return this;
}
public Builder withDoors(int doors){
this.doors = doors;
return this;
}
public Builder withWheels(int wheels){
this.wheels = wheels;
return this;
}
public Builder withType(String type){
this.type = type;
return this;
}
public Builder withConstructor(String constructor){
this.constructor = constructor;
return this;
}
public CarAlternative build(){
return new CarAlternative(this);
}
}
Schauen wir uns die Klasse an. Hier erstelle ich meinen Builder direkt als static inner Class des Objekts. Den eigentlichen Konstruktor kann ich wie in meinem Fall auf private setzen. Damit bestimme ich, dass die Klasse nur noch über den Builder erstellt werden kann. Dies kann aber individuell verwendet werden.
Der Builder ist fast mit der CarBuilder-Klasse identisch. Schauen wir uns nun den Aufruf an:
CarAlternative carAlternative = new
carAlternative.Builder().withKw(150).withDoors(5).withWheels(4)
.withConstructor("Audi").withType("A3").build();
Ich rufe die statische innere Klasse direkt über meine zu erstellende Klasse auf. Dort werden die ganzen Parameter gesetzt. Sind diese gesetzt, wird der private Konstruktor mit dem Builder aufgerufen und die erstellte Klasse zurückgegeben.
Insgesamt finde ich diese Version angenehmer als das eigentliche Pattern. Jedoch hat man ein wenig mehr Aufwand bei der Implementierung. Die Komplexität steigt zwar, aber man kann sich sicher sein, dass alle vorgesehenen Parameter gesetzt sind.
Habt ihr noch Fragen zum Builder Pattern, dann hinterlasst doch einen Kommentar.
Programmieren mit JAVAOracle Java wird immer mehr zum Standard in der Programmierung von Applikationen für jede erdenkliche Art von Plattform. Vom kleinsten mobilen Gerät, der Geräte- oder Fahrzeugautomation bis hin zu geschäftskritischen Applikationen: Mit Java sind Sie auf der sicheren Seite. Unsere Kurse führen Sie durch die gesamten Zertifizierungsprüfungen. |
Oracle Java wird immer mehr zum Standard in der Programmierung von Applikationen für jede erdenkliche Art von Plattform. Vom kleinsten mobilen Gerät, der Geräte- oder Fahrzeugautomation bis hin zu geschäftskritischen Applikationen: Mit Java sind Sie auf der sicheren Seite. Unsere Kurse führen Sie durch die gesamten Zertifizierungsprüfungen.