I will start with a simple example where we want to have an AbstractBuilder for building Pet instances, in our example this will be a Dog.
Here are the Pet class, and the Dog class that extends Pet
class Pet {
protected final String name;
Pet(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Dog extends Pet {
private final String color;
Dog(String name, String color) {
super(name);
this.color = color;
}
String getColor() {
return color;
}
@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + ", color='" + color + '\'' + '}';
}
}
Suppose we need a builder for building Dog instances and we know that in future there will be other Pet subclasses (like Cat, Snake…) and it will be good if we can avoid carrying builders methods withName
on all the builders.
We are going to use the AbstractBuilder pattern, and it will look like:
abstract class AbstractBuilder<T extends Pet> {
protected String name;
public AbstractBuilder<T> withName(String name) {
this.name = name;
return this;
}
public abstract T build();
}
And the DobBuilder:
class DogBuilder extends AbstractBuilder<Dog> {
private String color;
public DogBuilder withColor(String color) {
this.color = color;
return this;
}
@Override
public Dog build() {
return new Dog(name, color);
}
}
And this is how we use it:
Dog dog = new DogBuilder().withColor("black").withName("Dog").build();
But there is a problem, try call methods in another order:
Dog dog = new DogBuilder().withName("Dog").withColor("black").build();
Will not work because withName
returns and instance of AbstractBuilder<E>
which does not have withColor
method.
In order to fix this, we want withName
to return an instance of subclass that implements the AbstractBuilder
, in our case when calling withName
on a instance of DogBuilder
it should return DogBuilder
instead of AbstractBuilder<E>
.
A way to do this is by using Generics:
abstract class AbstractBuilder<T extends Pet, B extends AbstractBuilder<T, B>> {
protected String name;
public B withName(String name) {
this.name = name;
return (B) this;
}
public abstract T build();
}
We changed the AbstractBuilder
to have a generic type B
which extends AbstractBuilder
and on the builder methods there is an unchecked cast to type B
.
The DogBuilder now looks like:
class DogBuilder extends AbstractBuilder<Dog, DogBuilder> {
private String color;
public DogBuilder withColor(String color) {
this.color = color;
return this;
}
@Override
public Dog build() {
return new Dog(name, color);
}
}
Now, we don’t have previous limitation when calling builder methods:
Dog dog1 = new DogBuilder()
.withColor("red")
.withName("Dog")
.build();
Dog dog2 = new DogBuilder()
.withName("Dog2")
.withColor("black")
.build();
Initially when I wrote this article, abstract builder was declared as follows:
abstract class AbstractBuilder<T extends Pet, B extends AbstractBuilder>
There is a problem with this approach, it will not catch some exceptions at compile time and they will end up being thrown at runtime.
Example:
class Cat extends Pet {
private final String color;
Cat(String name, String color) {
super(name);
this.color = color;
}
String getColor() {
return color;
}
@Override
public String toString() {
return "Cat{" + "name='" + name + '\'' + ", color='" + color + '\'' + '}';
}
}
class CatBuilder extends AbstractBuilder<Cat, DogBuilder> {
private String color;
public CatBuilder withColor(String color) {
this.color = color;
return this;
}
@Override
public Cat build() {
return new Cat(name, color);
}
}
public class Main {
public static void main(String[] args) {
new CatBuilder()
.withColor("red")
.withName("Dog")
.build();
}
}
//Exception in thread "main" java.lang.ClassCastException: CatBuilder cannot be cast to DogBuilder
It is important to say that B extends AbstractBuilder<T, B>
not just B extends AbstractBuilder
abstract class AbstractBuilder<T extends Pet, B extends AbstractBuilder<T, B>>
Thanks Teodor M. for catching this.