AbstractBuilder pattern that returns the subclass instance

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.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *

Acest site folosește Akismet pentru a reduce spamul. Află cum sunt procesate datele comentariilor tale.