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.
public class DataHolder {
public int a;
public int b;
public int c;
private DataHolder(Builder builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
}
public static Builder newBuilder() {
return new Builder();
}
public static final class Builder {
private int a;
private int b;
private int c;
private Builder() {
}
public Builder withA(int a) {
this.a = a;
return this;
}
public Builder withB(int b) {
this.b = b;
return this;
}
public Builder withC(int c) {
this.c = c;
return this;
}
public DataHolder build() {
return new DataHolder(this);
}
}
} |
public class DataHolder { public int a;
public int b;
public int c; private DataHolder(Builder builder) {
this.a = builder.a;
this.b = builder.b;
this.c = builder.c;
} public static Builder newBuilder() {
return new Builder();
} public static final class Builder {
private int a;
private int b;
private int c; private Builder() {
} public Builder withA(int a) {
this.a = a;
return this;
} public Builder withB(int b) {
this.b = b;
return this;
} public Builder withC(int c) {
this.c = c;
return this;
} public DataHolder build() {
return new DataHolder(this);
}
}
}
Dazu schreiben wir nun noch einen trivialen Validator.
@FunctionalInterface
public interface Validator {
boolean checkCombination(T dataHolder);
}
public class NotZeroValidator implements Validator {
@Override
public boolean checkCombination(DataHolder dataHolder) {
final boolean a = dataHolder.a != 0;
final boolean b = dataHolder.b != 0;
final boolean c = dataHolder.c != 0;
return (a && b && c);
}
} |
@FunctionalInterface
public interface Validator {
boolean checkCombination(T dataHolder);
} public class NotZeroValidator implements Validator {
@Override
public boolean checkCombination(DataHolder dataHolder) {
final boolean a = dataHolder.a != 0;
final boolean b = dataHolder.b != 0;
final boolean c = dataHolder.c != 0;
return (a && b && c);
}
}
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.
public class Main {
public static void main(String[] args) {
final DataHolder build = DataHolder.newBuilder()
.withA(1).withB(1).withC(1).build();
final boolean b = new NotZeroValidator().checkCombination(build);
System.out.println("b = " + b);
}
} |
public class Main {
public static void main(String[] args) { final DataHolder build = DataHolder.newBuilder()
.withA(1).withB(1).withC(1).build();
final boolean b = new NotZeroValidator().checkCombination(build);
System.out.println("b = " + b);
}
}
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.
public class DataHolder {
//SNIPP
public static final class Builder {
//SNIPP
public Optional build() {
final DataHolder dataHolder = new DataHolder(this);
final boolean b = new NotZeroValidator().checkCombination(dataHolder);
if (b) {
return Optional.of(dataHolder);
} else {
return Optional.empty();
}
}
}
} |
public class DataHolder { //SNIPP public static final class Builder { //SNIPP public Optional build() {
final DataHolder dataHolder = new DataHolder(this);
final boolean b = new NotZeroValidator().checkCombination(dataHolder);
if (b) {
return Optional.of(dataHolder);
} else {
return Optional.empty();
}
}
}
}
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.
public class Main {
public static void main(String[] args) {
final Optional holderOptional = DataHolder.newBuilder()
.withA(1).withB(1).withC(1).build();
System.out.println("holderOptional.isPresent() = "
+ holderOptional.isPresent());
}
} |
public class Main {
public static void main(String[] args) {
final Optional holderOptional = DataHolder.newBuilder()
.withA(1).withB(1).withC(1).build();
System.out.println("holderOptional.isPresent() = "
+ holderOptional.isPresent());
}
}
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.
public class BusinessRule01Validator implements Validator {
@Override
public boolean checkCombination(DataHolder dataHolder) {
return dataHolder.a + dataHolder.b + dataHolder.c == 3;
}
}
public class DataHolder {
// SNIPP
public static final class Builder {
// SNIPP
public Optional build() {
DataHolder dataHolder = new DataHolder(this);
boolean b = new NotZeroValidator().checkCombination(dataHolder);
boolean c = new BusinessRule01Validator().checkCombination(dataHolder);
if (b && c) {
return Optional.of(dataHolder);
} else {
return Optional.empty();
}
}
}
} |
public class BusinessRule01Validator implements Validator {
@Override
public boolean checkCombination(DataHolder dataHolder) {
return dataHolder.a + dataHolder.b + dataHolder.c == 3;
}
} public class DataHolder { // SNIPP public static final class Builder {
// SNIPP public Optional build() {
DataHolder dataHolder = new DataHolder(this);
boolean b = new NotZeroValidator().checkCombination(dataHolder);
boolean c = new BusinessRule01Validator().checkCombination(dataHolder);
if (b && c) {
return Optional.of(dataHolder);
} else {
return Optional.empty();
}
}
}
}
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.
public class DataHolder {
//SNIPP
public static final class Builder {
//SNIPP
//add manually - start
private List<Validator> validatorList = new ArrayList<>();
public Builder addValidator(Validator validator){
validatorList.add(validator);
return this;
}
public Optional build() {
final DataHolder dataHolder = new DataHolder(this);
return validatorList.stream()
.filter(v->!v.checkCombination(dataHolder))
.map(v->Optional.empty()) //check false
.findFirst()
.orElse(Optional.of(dataHolder));
}
//add manually - stop
}
} |
public class DataHolder { //SNIPP
public static final class Builder {
//SNIPP
//add manually - start
private List<Validator> validatorList = new ArrayList<>();
public Builder addValidator(Validator validator){
validatorList.add(validator);
return this;
} public Optional build() {
final DataHolder dataHolder = new DataHolder(this);
return validatorList.stream()
.filter(v->!v.checkCombination(dataHolder))
.map(v->Optional.empty()) //check false
.findFirst()
.orElse(Optional.of(dataHolder));
}
//add manually - stop }
}
Da es sich bei dem Interface Validator um ein FunctionalInterface handelt, kann man natürlich auch mit Lamdas arbeiten.
//classic
final DataHolder.Builder builder = DataHolder.newBuilder();
final Optional holderOptional = builder
.withA(1).withB(1).withC(1).build();
System.out.println(".isPresent() = " + holderOptional.isPresent());
//wrong, but no Validator added
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
builder
.addValidator(new NotZeroValidator())
.addValidator(new BusinessRule01Validator());
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
System.out.println(".isPresent() = " + builder.withC(1).build().isPresent());
//lamdas
final DataHolder.Builder builder = DataHolder.newBuilder();
final Optional holderOptional = builder
.withA(1).withB(1).withC(1).build();
System.out.println(".isPresent() = " + holderOptional.isPresent());
//wrong, but no Validator added
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
builder
.addValidator(dataHolder -> {
final boolean a = dataHolder.a != 0;
final boolean b = dataHolder.b != 0;
final boolean c = dataHolder.c != 0;
return (a && b && c);
})
.addValidator(dataHolder -> dataHolder.a + dataHolder.b + dataHolder.c == 3);
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
System.out.println(".isPresent() = " + builder.withC(1).build().isPresent()); |
//classic
final DataHolder.Builder builder = DataHolder.newBuilder(); final Optional holderOptional = builder
.withA(1).withB(1).withC(1).build();
System.out.println(".isPresent() = " + holderOptional.isPresent()); //wrong, but no Validator added
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent()); builder
.addValidator(new NotZeroValidator())
.addValidator(new BusinessRule01Validator());
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
System.out.println(".isPresent() = " + builder.withC(1).build().isPresent()); //lamdas
final DataHolder.Builder builder = DataHolder.newBuilder(); final Optional holderOptional = builder
.withA(1).withB(1).withC(1).build();
System.out.println(".isPresent() = " + holderOptional.isPresent()); //wrong, but no Validator added
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent()); builder
.addValidator(dataHolder -> {
final boolean a = dataHolder.a != 0;
final boolean b = dataHolder.b != 0;
final boolean c = dataHolder.c != 0;
return (a && b && c);
})
.addValidator(dataHolder -> dataHolder.a + dataHolder.b + dataHolder.c == 3);
System.out.println(".isPresent() = " + builder.withC(2).build().isPresent());
System.out.println(".isPresent() = " + builder.withC(1).build().isPresent());
Der nächste Schritt besteht nun darin, die Implementierung generischer zu gestalten.
Nennen wir die generische Builder-Implementierung CheckedBuilder.
public class CheckedBuilder<B, T> {
protected List<Validator> validatorList = new ArrayList<>();
public B addValidator(Validator validator) {
validatorList.add(validator);
return (B) this;
}
protected Optional checkAndGet(T value) {
return validatorList.stream()
.filter(v -> !v.checkCombination(value))
.map(v -> Optional.empty()) //check false
.findFirst()
.orElse(Optional.of(value));
}
} |
public class CheckedBuilder<B, T> { protected List<Validator> validatorList = new ArrayList<>(); public B addValidator(Validator validator) {
validatorList.add(validator);
return (B) this;
} protected Optional checkAndGet(T value) {
return validatorList.stream()
.filter(v -> !v.checkCombination(value))
.map(v -> Optional.empty()) //check false
.findFirst()
.orElse(Optional.of(value));
}
}
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.
public class DataHolder {
//SNIPP
//extend from CheckedBuilder
public static final class Builder extends CheckedBuilder<Builder, DataHolder> {
//SNIPP
//add manually - start
public Optional build() {
final DataHolder dataHolder = new DataHolder(this);
return checkAndGet(dataHolder);
}
//add manually - stop
}
} |
public class DataHolder { //SNIPP //extend from CheckedBuilder
public static final class Builder extends CheckedBuilder<Builder, DataHolder> {
//SNIPP //add manually - start
public Optional build() {
final DataHolder dataHolder = new DataHolder(this);
return checkAndGet(dataHolder);
}
//add manually - stop
}
}
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();
public abstract class CheckedBuilder<B, T> {
protected List<Validator> validatorList = new ArrayList<>();
public B addValidator(Validator validator) {
validatorList.add(validator);
return (B) this;
}
private Optional checkAndGet(T value) {
return validatorList.stream()
.filter(v -> !v.checkCombination(value))
.map(v -> Optional.empty()) //check false
.findFirst()
.orElse(Optional.of(value));
}
protected abstract T createInstance();
public Optional build() {
return checkAndGet(this.createInstance());
}
} |
public abstract class CheckedBuilder<B, T> { protected List<Validator> validatorList = new ArrayList<>(); public B addValidator(Validator validator) {
validatorList.add(validator);
return (B) this;
} private Optional checkAndGet(T value) {
return validatorList.stream()
.filter(v -> !v.checkCombination(value))
.map(v -> Optional.empty()) //check false
.findFirst()
.orElse(Optional.of(value));
} protected abstract T createInstance(); public Optional build() {
return checkAndGet(this.createInstance());
}
}
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.
public class DataHolder {
//SNIPP
//extend from CheckedBuilder
public static final class Builder extends CheckedBuilder<Builder, DataHolder> {
//SNIPP
//implement
@Override
public DataHolder createInstance() {
return new DataHolder(this);
}
//add manually - start
//delete build() method
//add manually - stop
}
} |
public class DataHolder { //SNIPP //extend from CheckedBuilder
public static final class Builder extends CheckedBuilder<Builder, DataHolder> {
//SNIPP
//implement
@Override
public DataHolder createInstance() {
return new DataHolder(this);
} //add manually - start
//delete build() method
//add manually - stop
}
}