Previously, I wrote a post about builder pattern. This post intends to extend the builder pattern further. Let’s start with previous working example of Employee class as follows.
import java.util.Date; public class Employee { private final int id; //required private final String name; //required private final String address; //optional private final String phoneNumber; //optional private final Date dateOfBirth; //optional private final String placeOfBirth; //optional private final String ssn; //optional private Employee(Builder builder) { this.id = builder.id; this.name = builder.name; this.address = builder.address; this.phoneNumber = builder.phoneNumber; this.dateOfBirth = builder.dateOfBirth; this.placeOfBirth = builder.placeOfBirth; this.ssn = builder.ssn; } public int getId() { return id; } public String getName() { return name; } public String getAddress() { return address; } public String getPhoneNumber() { return phoneNumber; } public Date getDateOfBirth() { return dateOfBirth; } public String getPlaceOfBirth() { return placeOfBirth; } public String getSsn() { return ssn; } public Builder unbuild() { return new Builder(this); } public static class Builder { private final int id; private final String name; private String address; private String phoneNumber; private Date dateOfBirth; private String placeOfBirth; private String ssn; public Builder(int id, String name) { this.id = id; this.name = name; } private Builder(Employee employee) { this.id = employee.getId(); this.name = employee.getName(); this.address = employee.getAddress(); this.phoneNumber = employee.getPhoneNumber(); this.dateOfBirth = employee.getDateOfBirth(); this.placeOfBirth = employee.getPlaceOfBirth(); this.ssn = employee.getSsn(); } public Builder ssn(String ssn) { this.ssn = ssn; return this; } public Builder address(String address) { this.address = address; return this; } public Builder phoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; return this; } public Builder dateOfBirth(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; return this; } public Builder placeOfBirth(String placeOfBirth) { this.placeOfBirth = placeOfBirth; return this; } public Employee build() { return new Employee(this); } } }
So what’s the problem with this implementation? If we want the users of this class to actually initialize all field values properly, then we can’t enforce the users to do so since users can just call the builder to instantiate an object of Employee as follows.
Employee employee = Employee.builder(1, "Watson") .address("Baker street, London") .phoneNumber("+1234567890") .build();
employee object has only id, name, address, and phoneNumber filled, and it is a valid call. The question would be, how to prevent such call being made? Guided builder pattern gives you the answer.
Guided builder pattern is intended to guide the users of the class in instantiating an object. This can be done by, first, making the static Builder class to have 1) a default constructor (without parameter), 2) constructor that accept Employee as parameter, and then remove final from both id and name field.
private int id; private String name; private String address; private String phoneNumber; private Date dateOfBirth; private String placeOfBirth; private String ssn; private Builder() {} private Builder(Employee employee) { this.id = employee.getId(); this.name = employee.getName(); this.address = employee.getAddress(); this.phoneNumber = employee.getPhoneNumber(); this.dateOfBirth = employee.getDateOfBirth(); this.placeOfBirth = employee.getPlaceOfBirth(); this.ssn = employee.getSsn(); }
Second, we create interfaces which each has a field assignment method. The return type of the field assignment method in each interface is another interface that assigns other field. This is done until all interfaces to assign fields are implemented. The last interface returns the static Builder class.
public interface IdBuilder { NameBuilder id(int id); } public interface NameBuilder { AddressBuilder name(String name); } public interface AddressBuilder { PhoneNumberBuilder address(String address); } public interface PhoneNumberBuilder { DateOfBirthBuilder phoneNumber(String phoneNumber); } public interface DateOfBirthBuilder { PlaceOfBirthBuilder dateOfBirth(Date dateOfBirth); } public interface PlaceOfBirthBuilder { SsnBuilder placeOfBirth(String placeOfBirth); } public interface SsnBuilder { Builder ssn(String ssn); }
Third, the builder and unbuild methods in the class need to return the first interface type.
public static IdBuilder builder() { return new Builder(); } public IdBuilder unbuild() { return new Builder(this); }
Last, the static Builder class needs to implement those interfaces.
public static class Builder implements IdBuilder, NameBuilder, AddressBuilder, PhoneNumberBuilder, DateOfBirthBuilder, PlaceOfBirthBuilder, SsnBuilder{ private int id; private String name; private String address; private String phoneNumber; private Date dateOfBirth; private String placeOfBirth; private String ssn; private Builder() {} private Builder(Employee employee) { this.id = employee.getId(); this.name = employee.getName(); this.address = employee.getAddress(); this.phoneNumber = employee.getPhoneNumber(); this.dateOfBirth = employee.getDateOfBirth(); this.placeOfBirth = employee.getPlaceOfBirth(); this.ssn = employee.getSsn(); } public NameBuilder id(int id) { this.id = id; return this; } public AddressBuilder name(String name) { this.name = name; return this; } public PhoneNumberBuilder address(String address) { this.address = address; return this; } public DateOfBirthBuilder phoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; return this; } public PlaceOfBirthBuilder dateOfBirth(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; return this; } public SsnBuilder placeOfBirth(String placeOfBirth) { this.placeOfBirth = placeOfBirth; return this; } public Builder ssn(String ssn) { this.ssn = ssn; return this; } public Employee build() { return new Employee(this); } }
By doing this, it only allows the users of the class to instantiate object as follows.
Employee employee = Employee.builder() .id(1234) .name("Watson") .address("Baker street, London") .phoneNumber("+1234567890") .dateOfBirth(new Date()) .placeOfBirth("12345") .ssn("123121323") .build();
Another handy thing is that we can enforce certain value NOT to be accepted by using Guava checkNotNull, for instance, within the implementation of the field assignment methods.
The complete Employee class implementation is as follows.
import java.util.Date; public class Employee { private final int id; private final String name; private final String address; private final String phoneNumber; private final Date dateOfBirth; private final String placeOfBirth; private final String ssn; private Employee(Builder builder) { this.id = builder.id; this.name = builder.name; this.address = builder.address; this.phoneNumber = builder.phoneNumber; this.dateOfBirth = builder.dateOfBirth; this.placeOfBirth = builder.placeOfBirth; this.ssn = builder.ssn; } public int getId() { return id; } public String getName() { return name; } public String getAddress() { return address; } public String getPhoneNumber() { return phoneNumber; } public Date getDateOfBirth() { return dateOfBirth; } public String getPlaceOfBirth() { return placeOfBirth; } public String getSsn() { return ssn; } public static IdBuilder builder() { return new Builder(); } public IdBuilder unbuild() { return new Builder(this); } public interface IdBuilder { NameBuilder id(int id); } public interface NameBuilder { AddressBuilder name(String name); } public interface AddressBuilder { PhoneNumberBuilder address(String address); } public interface PhoneNumberBuilder { DateOfBirthBuilder phoneNumber(String phoneNumber); } public interface DateOfBirthBuilder { PlaceOfBirthBuilder dateOfBirth(Date dateOfBirth); } public interface PlaceOfBirthBuilder { SsnBuilder placeOfBirth(String placeOfBirth); } public interface SsnBuilder { Builder ssn(String ssn); } public static class Builder implements IdBuilder, NameBuilder, AddressBuilder, PhoneNumberBuilder, DateOfBirthBuilder, PlaceOfBirthBuilder, SsnBuilder{ private int id; private String name; private String address; private String phoneNumber; private Date dateOfBirth; private String placeOfBirth; private String ssn; private Builder() {} private Builder(Employee employee) { this.id = employee.getId(); this.name = employee.getName(); this.address = employee.getAddress(); this.phoneNumber = employee.getPhoneNumber(); this.dateOfBirth = employee.getDateOfBirth(); this.placeOfBirth = employee.getPlaceOfBirth(); this.ssn = employee.getSsn(); } public NameBuilder id(int id) { this.id = id; return this; } public AddressBuilder name(String name) { this.name = name; return this; } public PhoneNumberBuilder address(String address) { this.address = address; return this; } public DateOfBirthBuilder phoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; return this; } public PlaceOfBirthBuilder dateOfBirth(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; return this; } public SsnBuilder placeOfBirth(String placeOfBirth) { this.placeOfBirth = placeOfBirth; return this; } public Builder ssn(String ssn) { this.ssn = ssn; return this; } public Employee build() { return new Employee(this); } } }
I hope this post is useful.