All Articles

classes, builders and invariants

Say you have a class User. The user has an email, a state and a list of authentication methods, among other fields:

public class User {
  private String email = "";
  private List<String> authenticationMethods = Collections.emptyList();
  private State state = State.INITIAL;

  public String email() {
    return email;
  }

  public List<String> authenticationMethods() {
    return authenticationMethods;
  }

  public State state() {
    return state;
  }
}

There’s some initial state invariants. The email cannot be null or empty, the list of of authentication methods cannot be null and the state is non-null with an initial value for new objects of INITIAL. You decide to use a builder and so move the preconditions there:

public class User {
  private String email;
  private List<String> authenticationMethods;
  private State state;

  public User(Builder builder) {
    this.email = builder.email;
    this.authenticationMethods = ImmutableList.copyOf(builder.authenticationMethods);
    this.state = builder.state;
  }

  public String email() {
    return email;
  }

  public List<String> authenticationMethods() {
    return authenticationMethods;
  }

  public State state() {
    return state;
  }
  
  public static class Builder {
    private String email = "";
    private List<String> authenticationMethods = new ArrayList<>();
    private State state = State.INITIAL;

    public Builder() {}

    public Builder email(String email) {
      this.email = checkNotNullOrEmpty(email);
      return this;
    }

    public Builder addAuthenticationMethod(String authenticationMethod) {
      this.authenticationMethods.add(checkNotNullOrEmpty(authenticationMethod));
      return this;
    }

    public Builder state(State state) {
      this.state = checkNotNull(state);
      return this;
    }

    public User build() {
      return new User(this);
    }
  }
}

Good. You then want to add a no-arg constructor. Now you have a problem. You can either add the same preconditions to the class fields, as in the builder, in which case you end up with a brittle copy of constants, or break the consistency of the object’s initial state when constructed with the builder vs the no-arg constructor.

There’s a simple way out: use an instance of the Builder in your no-arg constructors:

public class User {
  private String email;
  private List<String> authenticationMethods;
  private State state;

  public User() {
    this(new Builder());
  }

  public User(Builder builder) {
    this.email = builder.email;
    this.authenticationMethods = ImmutableList.copyOf(builder.authenticationMethods);
    this.state = builder.state;
  }
  
  ...
}