//

Checked Builder Pattern in Java

4.5.2015 | 5 Minuten Lesezeit

Bei der Verwendung des Builder Pattern gibt es immer wieder die Herausforderung,
dass man bei der Erzeugung der finalen Instanz die Gültigkeit aller vorangegangenen
Schritte überprüfen muss. Anders formuliert: Wurde eine gültige Kombination der
Methoden, die die Attribute setzen, verwendet? Dazu sehen wir uns das nachfolgende Beispiel einmal genauer an.
Wir haben zum einen die Klasse, von der Instanzen erzeugt werden sollen. Hier wurde auch gleich ein passender Builder generiert.

1public class DataHolder {
2 
3  public int a;
4  public int b;
5  public int c;
6 
7  private DataHolder(Builder builder) {
8    this.a = builder.a;
9    this.b = builder.b;
10    this.c = builder.c;
11  }
12 
13  public static Builder newBuilder() {
14    return new Builder();
15  }
16 
17  public static final class Builder {
18    private int a;
19    private int b;
20    private int c;
21 
22    private Builder() {
23    }
24 
25    public Builder withA(int a) {
26      this.a = a;
27      return this;
28    }
29 
30    public Builder withB(int b) {
31      this.b = b;
32      return this;
33    }
34 
35    public Builder withC(int c) {
36      this.c = c;
37      return this;
38    }
39 
40    public DataHolder build() {
41      return new DataHolder(this);
42    }
43  }
44}
45

Dazu schreiben wir nun noch einen trivialen Validator.

1@FunctionalInterface
2public interface Validator {
3  boolean checkCombination(T dataHolder);
4}
5 
6public class NotZeroValidator implements Validator {
7  @Override
8  public boolean checkCombination(DataHolder dataHolder) {
9    final boolean a = dataHolder.a != 0;
10    final boolean b = dataHolder.b != 0;
11    final boolean c = dataHolder.c != 0;
12    return (a && b && c);
13  }
14}
15

Hier soll lediglich sichergestellt sein, dass die Values nicht alle gleichzeitig 0 sein werden. (Über den Sinn kann man natürlich beliebig lange diskutieren. 😉 )

Die Anwendung kann dann wie folgt aussehen.

1public class Main {
2  public static void main(String[] args) {
3 
4    final DataHolder build = DataHolder.newBuilder()
5        .withA(1).withB(1).withC(1).build();
6    final boolean b = new NotZeroValidator().checkCombination(build);
7    System.out.println("b = " + b);
8  }
9}
10

Allerdings hat dies einige Schwächen. Hier muss man davon ausgehen, dass jeder beteiligte Entwickler es auch kennt und machen wird. Da wir über einen Builder verfügen, liegt es nahe, dies in die Methode build() zu verlegen.
Hierzu modifizieren wir den Builder.

1public class DataHolder {
2 
3//SNIPP
4 
5  public static final class Builder {
6 
7//SNIPP
8 
9    public Optional build() {
10      final DataHolder dataHolder = new DataHolder(this);
11      final boolean b = new NotZeroValidator().checkCombination(dataHolder);
12      if (b) {
13        return Optional.of(dataHolder);
14      } else {
15        return Optional.empty();
16      }
17    }
18  }
19}
20

Ich ändere hier den Rückgabetyp in eine Instanz der Klasse Optional um, um ein null zu vermeiden und nicht im Fehlerfall eine Exception werfen zu müssen.

Die Verwendung ändert sich damit nur geringfügig.

1public class Main {
2  public static void main(String[] args) {
3    final Optional holderOptional = DataHolder.newBuilder()
4        .withA(1).withB(1).withC(1).build();
5    System.out.println("holderOptional.isPresent() = " 
6        + holderOptional.isPresent());
7  }
8}
9

Nun wird es sicherlich nicht nur eine Regel geben, die es zu beachten gilt. Also ist der nächste Schritt, eine Menge von Validatoren zu verwenden. Erzeugen wir uns deshalb einen zweiten Validator und fügen diesen der Methode build() hinzu.

1public class BusinessRule01Validator implements Validator {
2  @Override
3  public boolean checkCombination(DataHolder dataHolder) {
4    return dataHolder.a + dataHolder.b + dataHolder.c == 3;
5  }
6}
7 
8public class DataHolder {
9 
10// SNIPP
11 
12    public static final class Builder {
13 
14// SNIPP
15 
16    public Optional build() {
17      DataHolder dataHolder = new DataHolder(this);
18      boolean b = new NotZeroValidator().checkCombination(dataHolder);
19      boolean c = new BusinessRule01Validator().checkCombination(dataHolder);
20      if (b && c) {
21        return Optional.of(dataHolder);
22      } else {
23        return Optional.empty();
24      }
25    }
26  }
27}
28

Da es sich allerdings um eine größere Menge von Validatoren handeln kann, ist hier eine Liste von Validatoren sinnvoller. Die Liste der Validatoren halten wir in dem jeweiligen Builder vor. Zusätzlich bekommt man die Möglichkeit, zur Laufzeit Validatoren hinzuzufügen und zu entfernen.

1public class DataHolder {
2 
3  //SNIPP
4 
5  public static final class Builder {
6 
7  //SNIPP
8 
9    //add manually - start
10    private List<Validator> validatorList = new ArrayList<>();
11    public Builder addValidator(Validator validator){
12      validatorList.add(validator);
13      return this;
14    }
15 
16    public Optional build() {
17      final DataHolder dataHolder = new DataHolder(this);
18      return validatorList.stream()
19          .filter(v->!v.checkCombination(dataHolder))
20          .map(v->Optional.empty()) //check false
21          .findFirst()
22          .orElse(Optional.of(dataHolder));
23    }
24    //add manually - stop
25 
26  }
27}
28

Da es sich bei dem Interface Validator um ein FunctionalInterface handelt, kann man natürlich auch mit Lamdas arbeiten.

1//classic
2    final DataHolder.Builder builder = DataHolder.newBuilder();
3 
4    final Optional holderOptional = builder
5        .withA(1).withB(1).withC(1).build();
6    System.out.println(".isPresent() = " + holderOptional.isPresent());
7 
8    //wrong, but no Validator added
9    System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
10 
11 
12    builder
13        .addValidator(new NotZeroValidator())
14        .addValidator(new BusinessRule01Validator());
15    System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
16    System.out.println(".isPresent() = " + builder.withC(1).build().isPresent());
17 
18//lamdas
19    final DataHolder.Builder builder = DataHolder.newBuilder();
20 
21    final Optional holderOptional = builder
22        .withA(1).withB(1).withC(1).build();
23    System.out.println(".isPresent() = " + holderOptional.isPresent());
24 
25    //wrong, but no Validator added
26    System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
27 
28    builder
29        .addValidator(dataHolder -> {
30          final boolean a = dataHolder.a != 0;
31          final boolean b = dataHolder.b != 0;
32          final boolean c = dataHolder.c != 0;
33          return (a && b && c);
34        })
35        .addValidator(dataHolder -> dataHolder.a + dataHolder.b + dataHolder.c == 3);
36    System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
37    System.out.println(".isPresent() = " + builder.withC(1).build().isPresent());
38

Der nächste Schritt besteht nun darin, die Implementierung generischer zu gestalten.
Nennen wir die generische Builder-Implementierung CheckedBuilder.

1public class CheckedBuilder<B, T> {
2 
3  protected List<Validator> validatorList = new ArrayList<>();
4 
5  public B addValidator(Validator validator) {
6    validatorList.add(validator);
7    return (B) this;
8  }
9 
10  protected Optional checkAndGet(T value) {
11    return validatorList.stream()
12        .filter(v -> !v.checkCombination(value))
13        .map(v -> Optional.empty()) //check false
14        .findFirst()
15        .orElse(Optional.of(value));
16  }
17}
18

Damit muss die generierte spezielle Builder-Implementierung nun nur noch minimal
angepasst werden. Der Builder muss von CheckedBuilder erben und in der Methode
build() die Methode checkAndGet(T value) aufrufen.

1public class DataHolder {
2 
3//SNIPP
4 
5  //extend from CheckedBuilder
6  public static final class Builder extends CheckedBuilder<Builder, DataHolder> {
7 
8//SNIPP
9 
10    //add manually - start
11    public Optional build() {
12      final DataHolder dataHolder = new DataHolder(this);
13      return checkAndGet(dataHolder);
14    }
15    //add manually - stop
16  }
17}
18

Die Verwendung der Instanz der Klasse Builder erfolgt dann genauso wie vorher.

Only one more thing
Wenn man nicht die Methode build() in dieser Form editieren möchte, kann man auch einen etwas anderen Weg gehen.
Die Methode build() kann auch in den CheckedBuilder verlegt werden, so dass man sie in dem generierten Builder löschen muss. Der CheckedBuilder wird abstract deklariert und bekommt die Methoden protected abstract T createInstance();

1public abstract class CheckedBuilder<B, T> {
2 
3  protected List<Validator> validatorList = new ArrayList<>();
4 
5  public B addValidator(Validator validator) {
6    validatorList.add(validator);
7    return (B) this;
8  }
9 
10  private Optional checkAndGet(T value) {
11    return validatorList.stream()
12        .filter(v -> !v.checkCombination(value))
13        .map(v -> Optional.empty()) //check false
14        .findFirst()
15        .orElse(Optional.of(value));
16  }
17 
18  protected abstract T createInstance();
19 
20  public Optional build() {
21    return checkAndGet(this.createInstance());
22  }
23}
24

Damit verändert sich der Builder in der Form, dass man das Ganze sehr leicht in bestehende Templates einbauen kann. Die notwendigen Informationen zum Zeitpunkt des Generierens liegen vollständig vor. Nur leider nicht in der Form, dass man mittels Reflection innerhalb des CheckedBuilder darauf zugreifen kann.

1public class DataHolder {
2 
3 //SNIPP
4 
5  //extend from CheckedBuilder
6  public static final class Builder extends CheckedBuilder<Builder, DataHolder> {
7 
8 //SNIPP
9 
10    //implement
11    @Override
12    public DataHolder createInstance() {
13      return new DataHolder(this);
14    }
15 
16    //add manually - start
17    //delete build() method
18    //add manually - stop
19  }
20}
21

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.