Builders and Required Params
Ever since Effective Java 2 came out, the builder pattern became a defacto standard for instantiating objects with multiple parameters.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.fat(0)
.sodium(35)
.carbohydrate(27)
.build();
Required parameters are usually passed in via a constructor, as in the example above, but having too many might defeat the purpose of a builder and spread confusion all around. Can you tell what the two arguments above denote?
A way to avoid this is to provide a no-params constructor:
NutritionFacts cocaCola = new NutritionFacts.Builder() // No required params
.servingSize(240)
.servings(8)
.calories(100)
.fat(0)
.sodium(35)
.carbohydrate(27)
.build();
and validate the data whenever a build()
method is invoked:
public NutritionFacts build() {
if (servingSize <= 0) {
throw new IllegalStateException("serving size required");
}
if (servings <= 0) {
throw new IllegalStateException("number of servings required");
}
return new NutritionFacts(this);
}
The above works, but it would be nice to provide a fluid API where it is impossible to invoke build()
before the required parameters are provided.
Let’s describe the builder with a simple interface:
interface Builder {
Builder servingSize(int size);
Builder servings(int servings);
Builder calories(int calories);
Builder fat(int fat);
Builder sodium(int sodium);
Builder carbohydrate(int carbohydrate);
NutritionFacts build();
}
Recall the first two methods, servingSize(int)
and servings(int)
are required. Could we force the user to start the builder chain with them?
Suppose we declare two additional interfaces:
// Represents the first required param. Note the return type.
interface ServingSizeBuilder {
ServingBuilder servingSize(int size);
}
// Again, note the return type.
interface ServingBuilder {
Builder serving(int size);
}
Starting with a ServingSizeBuilder
instance, you’re now forced to start the chain with the two required params and are unable to invoke build()
before:
servingSizeBuilder.servingSize(someInt)
.serving(anotherInt)
// all other methods specified in a Builder.
We should remove the two required setters methods from the Builder
interface, as they are described in the two we defined above. We end up with:
interface Builder {
interface ServingSizeBuilder {
ServingBuilder servingSize(int size);
}
interface ServingBuilder {
Builder serving(int size);
}
// Note, no servingSize(int size) or serving(int size)
Builder calories(int calories);
Builder fat(int fat);
Builder sodium(int sodium);
Builder carbohydrate(int carbohydrate);
NutritionFacts build();
}
This can be fully appreciated when implemented:
class NutritionFactsBuilder implements Builder,
Builder.ServingSizeBuilder, Builder.ServingBuilder {
private int servingSize = 0;
private int servings = 0;
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
// To start the builder chain with required parameters,
// we need to provide the following static factory method
// returning ServingSizeBuilder.
public static ServingSizeBuilder builder() {
return new NutritionFactsBuilder();
}
private NutritionFactsBuilder() {}
@Override public ServingBuilder servingSize(int val) {
servingSize = val;
return this;
}
@Override public Builder serving(int val) {
servings = val;
return this;
}
@Override public Builder calories(int val) {
calories = val;
return this;
}
@Override public Builder fat(int val) {
fat = val;
return this;
}
@Override public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
@Override public Builder sodium(int val) {
sodium = val;
return this;
}
@Override public NutritionFacts build() {
return new NutritionFacts(this);
}
}
There it is! A builder where required params must be set before build()
method can be invoked.