How Builder Pattern simplifies object creation
If you have been a regular reader of my posts, you might have observed that “maintainability of code” has been one of our recurring themes. I believe that maintainability and simplicity of code are critical factors that dictate the success of the software in the long run.
It is obvious that an easy to maintain software is more likely to survive and evolve than a software with mangled code. As obvious as it sounds, don’t be surprised if you find software development teams ignore the quality of code in the rush to deliver more code. The insane deadlines and the apathy of the management towards programmers don’t help either.
There are many design patterns used in software development. If there is one that provides the most value in return for the least effort, it has to be the Builder pattern.
Why? Because Builder Pattern simplifies the object creation in your code. And your code is full of object creation right?
Object creation without using a design patterns
Object creation without using a pattern can only be done by using the constructors. In the sample below, a Customer can be created in many ways.
- Use the default constructor and use the setter’s to instantiate the id and the first name fields
- Use the 2nd constructor by passing id as parameter and use the setFirstname()
- Use the 3rd constructor by passing in both id and first name
public class Customer {
private Integer id;
private String firstname;
public Customer() {
}
public Customer(Integer id) {
this.id = id;
}
public Customer(Integer id, String firstname) {
this.id = id;
this.firstname = firstname;
}
// Getters and setters
}
The shortcoming of the above code is
- With just 2 attributes, we already have 3 combinations of the constructor
- Assuming that both id and first name were mandatory to the creation of the object, we would need to have only one constructor – the one that takes in both id and first name. Remember that the default no-arg constructor exists implicitly, so users could still get away using the no-arg constructor
- We could get away from the above issue by making the default no-arg constructor private, but that leads to more issues described next – the bloated constructor
- It is not easy to make some of the parameters mandatory and others optional
Bloated constructor
It is best described by extending the previous example.
Let us add a couple of more fields to the customer class – A String surname and a boolean customerStatus.
The new constructor with all parameters included will be
public Customer(Integer id, String firstname, String surname, boolean customerStatus) {
this.id = id;
this.firstname = firstname;
this.surname = surname;
this.customerStatus = customerStatus;
}
Add a couple of more fields, and you get the idea. Bloated constructor is the one that has too many parameters, thereby requiring the user to pass in a huge number of objects.
Builder Pattern
Builder pattern is a creational pattern.
There are many creational patterns – Abstract factory, Factory method, Singleton, Builder etc. What makes the Builder pattern standout is that it separates the formation of the object from the representation.
It means that you can create “instances” of objects having different values by using the same formation process.
As usual, the pattern is best explained using code.
Let us assume that as per the business logic, id, first name, surname are mandatory and customerStatus is optional with a default value of true.
public class CustomerBuilder {
// Mandatory parameters
private Integer id;
private String firstname;
private String surname;
/*
Optional parameters with default value.
After the object has been initialized, the default value
could be over ridden using withCustomerStatus()
*/
private boolean customerStatus = true;
// Only mandatory parameters are to be included in the constructor
public CustomerBuilder(Integer id, String firstname, String surname) {
this.id = id;
this.firstname = firstname;
this.surname = surname;
}
public CustomerBuilder withId(Integer id) {
this.id = id;
return this;
}
public CustomerBuilder withFirstName(String firstname) {
this.firstname = firstname;
return this;
}
public CustomerBuilder withSurname(String surname) {
this.surname = surname;
return this;
}
public CustomerBuilder withCustomerStatus(boolean customerStatus) {
this.customerStatus = customerStatus;
return this;
}
public Customer build() {
Customer customer = new Customer();
customer.setId(id);
customer.setFirstname(firstname);
customer.setSurname(surname);
customer.setCustomerStatus(customerStatus);
return customer;
}
}
The builder defines the same set of parameters as the Customer Object. The mandatory parameters are all part of the constructor.
The optional parameters may not may not have a default value, in our example the customerStatus has a default value of true.
Any of the parameters (including the optional ones) could be overridden by using the withXYZ() methods.
Once all the necessary fields have been populated, the build() method needs to be invoked. This method constructs a new instance of the Customer object, assigns all the values from the builder into the instance variables of the Customer object. The build() methods then returns the newly constructed object to the caller.
An invocation of the Customer builder may look like below.
Customer customer = new CustomerBuilder(1, "John", "West").build();
Incase we want to override the default value of the customerStatus, use it as below.
Customer customer = new CustomerBuilder(1, "Kevin", "Porter").withCustomerStatus(false).build();
Look how easy and readable the code has become. There was no need to manage the complexities of managing variety of constructors, setters, getters etc. Clean manageable code that also reduces complexity!
Builder invoking other builders
Most real world scenarios invoke complex hierarchy of objects, composite objects etc.
For example, consider that the Customer entity has an embedded Address entity.
Assuming that the Address entity also has a corresponding AddressBuilder, we could modify the CustomerBuilder as below.
public class CustomerBuilder {
// Collapsed code
private Address address = new AddressBuilder("1", "Roger Street", "Dallas").build();
// Collapsed code
}