Checked Builder

Bei der Verwendung des Builder Pattern gibt es immer wieder die Herausforderung, das man bei der Erzeugung der finalen Instanz die Gültigkeit aller vorhergegangenen Schritte überprüfen muss. Anders formuliert: wurde eine gültige Kombination der Methoden 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);
    }
  }
}

Dazu schreiben wir nun noch einen trivialen Validator.

@FunctionalInterface
public interface Validator<T> {
  boolean checkCombination(T dataHolder);
}

public class NotZeroValidator implements Validator<DataHolder> {
  @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 schergestellt sein, dass die Values nicht alle gleichzeitig 0 sein werden. (Über den Sinn kann man natürlich nun 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);
  }
}

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

public class DataHolder {

//SNIPP

  public static final class Builder {

//SNIPP

    public Optional<DataHolder> 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 heri die Rückgabetyp auf eine Instanz der Klasse Optional um zum einen 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<DataHolder> 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 gibt. Also ist der nächste Schritt, eine Menge von Validatoren zu verwenden. Erzeugen wir uns deshalt einen zweiten Validator und fügen diesen der Methode build() hinzu.

public class BusinessRule01Validator implements Validator<DataHolder> {
  @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<DataHolder> 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, macht hier eine Liste von Validatoren mehr Sinn. 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<DataHolder>> validatorList = new ArrayList<>();
    public Builder addValidator(Validator<DataHolder> validator){
      validatorList.add(validator);
      return this;
    }

    public Optional<DataHolder> build() {
      final DataHolder dataHolder = new DataHolder(this);
      return validatorList.stream()
          .filter(v->!v.checkCombination(dataHolder))
          .map(v->Optional.<DataHolder>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<DataHolder> 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<DataHolder> 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 gestallten. Nennen wir die generische Builder-Implementierung CheckedBuilder.

public class CheckedBuilder<B extends CheckedBuilder, T> {

  protected List<Validator<T>> validatorList = new ArrayList<>();

  public B addValidator(Validator<T> validator) {
    validatorList.add(validator);
    return (B) this;
  }

  protected Optional<T> checkAndGet(T value) {
    return validatorList.stream()
        .filter(v -> !v.checkCombination(value))
        .map(v -> Optional.<T>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<DataHolder> 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 das man in dem generierten Builder diese löschen muss. Der CheckedBuilder wird abstract deklariert und bekommt die Methoden protected abstract T createInstance();

public abstract class CheckedBuilder<B extends CheckedBuilder, T> {

  protected List<Validator<T>> validatorList = new ArrayList<>();

  public B addValidator(Validator<T> validator) {
    validatorList.add(validator);
    return (B) this;
  }

  private Optional<T> checkAndGet(T value) {
    return validatorList.stream()
        .filter(v -> !v.checkCombination(value))
        .map(v -> Optional.<T>empty()) //check false
        .findFirst()
        .orElse(Optional.of(value));
  }

  protected abstract T createInstance();

  public Optional<T> build() {
    return checkAndGet(this.createInstance());
  }
}

Damit verändert sich der Builder in der Form, das man es 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
  }
}

Youtube Video

Zu diesem Pattern gibt es ein YouTube Video frei zugänglich.

Youtube - CheckedBuilder

results matching ""

    No results matching ""