1. Overview
Previously, we've looked at the builder pattern in its general form. The implemented builder was a public static inner class declared inside the target class. Also, we didn't use an interface.
In this tutorial, we'll look at how we can use interfaces with the builder pattern and extend the existing builder implementations.
2. First Iteration
We'll start with a base data class and one of its implementations.
public abstract class Profession {
private final String name;
private final double salary;
private final List<String> duties;
public Profession(String name, double salary, List<String> duties) {
this.name = name;
this.salary = salary;
this.duties = duties == null ? new ArrayList<>() : new ArrayList<>(duties);
}
public abstract void work();
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public List<String> getDuties() {
return Collections.unmodifiableList(duties);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("name", name)
.append("Salary", salary)
.append("Duties", duties)
.toString();
}
}
public class Engineer extends Profession {
public Engineer(String name, double salary, List<String> duties) {
super(name, salary, duties);
}
@Override
public void work() {
System.out.println("Implementing and testing...");
}
}
Here, we have the abstract Profession class, and Engineer is an implementation of it. Throughout the tutorial, we'll also provide other implementations of Profession.
Next, we'll create a builder interface for the Profession objects.
public interface ProfessionBuilder<T extends Profession> {
ProfessionBuilder<T> name(String name);
ProfessionBuilder<T> salary(double salary);
ProfessionBuilder<T> duty(String duty);
T build();
}
Here, we're defining the methods for the common fields of Profession.
Then let's provide an implementation of ProfessionBuilder.
public class EngineerBuilder implements ProfessionBuilder<Engineer> {
String name;
double salary;
List<String> duties = new ArrayList<>();
@Override
public ProfessionBuilder<Engineer> name(String name) {
this.name = name;
return this;
}
@Override
public ProfessionBuilder<Engineer> salary(double salary) {
this.salary = salary;
return this;
}
@Override
public ProfessionBuilder<Engineer> duty(String duty) {
duties.add(duty);
return this;
}
public Engineer build() {
return new Engineer(name, salary, duties);
}
}
EngineerBuilder implements ProfessionBuilder<Engineer>. As a result, it builds Engineer objects.
In the current state, we have one data class and one builder. Next, we'll add another data class to the class hierarchy and see how other classes will evolve.
3. Second Iteration
We'll now introduce the Pilot class. The Engineer and Pilot classes have the same fields but different behaviors.
public class Pilot extends Profession {
public Pilot(String name, double salary, List<String> duties) {
super(name, salary, duties);
}
@Override
public void work() {
System.out.println("Flying the plane...");
}
}
At this point, we can create another builder for Pilot, but that would lead to code duplication. Instead, we can extract the common builder methods to an abstract class. Then concrete builder implementations should just implement the build method to return the appropriate object.
/**
* Pilot Builder
*/
public class PilotBuilder extends AbstractProfessionBuilder<Pilot> {
@Override
protected Pilot internalBuild() {
return new Pilot(this.name, this.salary, this.duties);
}
}
/**
* Engineer Builder
*/
public class EngineerBuilder extends AbstractProfessionBuilder<Engineer> {
@Override
protected Engineer internalBuild() {
return new Engineer(this.name, this.salary, this.duties);
}
}
public abstract class AbstractProfessionBuilder<T extends Profession> implements ProfessionBuilder<T> {
String name;
double salary;
List<String> duties = new ArrayList<>();
@Override
public ProfessionBuilder<T> name(String name) {
this.name = name;
return this;
}
@Override
public ProfessionBuilder<T> salary(double salary) {
this.salary = salary;
return this;
}
@Override
public ProfessionBuilder<T> duty(String duty) {
duties.add(duty);
return this;
}
public T build() {
return internalBuild();
}
protected abstract T internalBuild();
}
At the end of the second iteration, we have two Professions and two ProfessionBuilders. Moreover, each class hierarchy has a base class to reuse common functionality.
However, we have an important limitation in our design. For example, the Engineer and Pilot can have different fields and thus will certainly need different builder methods. With our current classes, we can't support this behavior.
4. Third Iteration
In this final iteration, we'll change our builder design to support builder methods that are tailored for specific builders.
Let's say, we have added new fields to both Engineer and Pilot.
public class Engineer extends Profession {
private final String tools;
public Engineer(String name, double salary, List<String> duties, String tools) {
super(name, salary, duties);
this.tools = tools;
}
@Override
public void work() {
System.out.println("Implementing and testing...");
}
}
public class Pilot extends Profession {
private final String language;
public Pilot(String name, double salary, List<String> duties, String language) {
super(name, salary, duties);
this.language = language;
}
@Override
public void work() {
System.out.println("Flying the plane...");
}
}
Our previous builders can't keep up with this new class structure. Our generic builder interface ProfessionBuilder doesn't have a language method for Pilot or doesn't have a tools method for Engineer. Even if we remove the interface and implement just a concrete builder, the problem is obvious: We'll have a common builder class with common methods like name, salary, and duty. Furthermore, these methods will return a reference to the container builder class - return this; - and not to the specific builder class.
So primarily we must change our builder interface:
public interface ProfessionBuilder<SELF extends ProfessionBuilder<SELF, TTarget>,
TTarget extends Profession> {
SELF name(String name);
SELF salary(double salary);
SELF duty(String duty);
TTarget build();
}
Here, we're also adding another generic type parameter, SELF. This will hold the reference to the actual builder class.
/**
* Pilot Builder
*/
public class PilotBuilder extends AbstractProfessionBuilder<PilotBuilder, Pilot> {
private String language;
public PilotBuilder language(String language) {
this.language = language;
return this;
}
@Override
protected Pilot internalBuild() {
return new Pilot(this.name, this.salary, this.duties, this.language);
}
}
/**
* Engineer Builder
*/
public class EngineerBuilder extends AbstractProfessionBuilder<EngineerBuilder, Engineer> {
private String tools;
public EngineerBuilder tools(String tools) {
this.tools = tools;
return this;
}
@Override
protected Engineer internalBuild() {
return new Engineer(this.name, this.salary, this.duties, this.tools);
}
}
public abstract class AbstractProfessionBuilder<SELF extends ProfessionBuilder<SELF, TTarget>,
TTarget extends Profession> implements ProfessionBuilder<SELF, TTarget> {
String name;
double salary;
List<String> duties = new ArrayList<>();
@Override
public SELF name(String name) {
this.name = name;
return self();
}
@Override
public SELF salary(double salary) {
this.salary = salary;
return self();
}
@Override
public SELF duty(String duty) {
duties.add(duty);
return self();
}
public TTarget build() {
return internalBuild();
}
protected abstract TTarget internalBuild();
private SELF self() {
return (SELF) this;
}
}
Note that we're casting the builder instance to the actual type that is defined during compile time. For this purpose, we have the self method.
As a result, the subclass builders can add their own custom methods while using the common base class.
5. Summary
In this tutorial, we've looked at how we can apply the builder pattern using interfaces and generics.
Finally, check out the source code for all examples in this tutorial over on Github.