Builder
Never allow an object to be constructed unless it's correct.
Builder:
- Collects multiple values, then maps them into a single validated value in a
build()
method. - Avoids partial objects – don't allow yourself to have an instance of the target type unless it is correct.
Example: Data where intValue
must be greater than byteValue
.
Switch from:
public class Data {
private final byte byteValue;
private final int intValue;
private final Optional<String> stringValue;
public Data(byte byteValue, int intValue, Optional<String> stringValue) {
this.byteValue = byteValue;
this.intValue = intValue;
this.stringValue = stringValue;
}
// ..
}
// Usage
Data data = new Data(0, 1, Optional.empty());
to:
public class Data {
private final byte byteValue;
private final int intValue;
private final Optional<String> stringValue;
// Package private to allow builder class access.
Data(byte byteValue, int intValue, Optional<String> stringValue) {
this.byteValue = byteValue;
this.intValue = intValue;
this.stringValue = stringValue;
}
// ..
}
public class DataBuilder {
private final Optional<Byte> byteValue;
private final Optional<Integer> intValue;
private final Optional<String> stringValue;
// Constructor
// either:
public DataBuilder() {
this.byteValue = Optional.empty();
this.integerValue = Optional.empty();
this.stringValue = Optional.empty();
}
// or
public DataBuilder(byte byteValue, int intValue) {
this.byteValue = Optional.of((Byte) byteValue);
this.integerValue = Optional.of((Integer) intValue);
this.stringValue = Optional.empty();
}
// Append additional values.
public DataBuilder withByteValue(byte byteValue) {
this.byteValue = Optional.of(byteValue);
return this;
}
public DataBuilder withIntValue(int intValue) {
this.intValue = Optional.of((Integer) intValue);
return this;
}
public DataBuilder withStringValue(String stringValue) {
this.stringValue = Optional.of(stringValue);
return this;
}
public Data build() throws IncompatibleValuesException {
// Default the byteValue and intValue
byte byteValue = this.byteValue
.orElse(0)
.byteValue();
int intValue = this.intValue
.orElse((int) byteValue + 1)
.intValue();
if (intValue < (int) byteValue) {
throw new IncompatibleValuesException(
intValue,
byteValue,
"intValue must not be smaller than byteValue.");
}
return new Data(byteValue, intValue, this.stringValue());
}
// ..
}
// Usage
Data data = new DataBuilder()
.withByteValue(123) // allows method chaining
.withIntValue(456)
.withStringValue("abc")
.build(); // enforces constraints
// so any error is raised here
Bug Variants Addressed
- 1: Raises an error1 at the point of building.
- 2: Reduces provider code working with invalid input, because only valid input reaches the business logic.
- 4: Only allow groupings of valid values in the constructed type.
- 6: Constrains input to a range of safe values.
1 It is not a bug to raise an error.