The Builder design pattern allows you to create objects that are usually complex using another object that builds them step by step.
This Builder pattern is used in situations where an object must be built repeatedly or when this object has lots of attributes and associated objects, and where using constructors to create the object is not a convenient solution.
This is a very useful design pattern also in test execution (unit test, for example) where we must create the object with valid or default attributes.
It usually solves the problem of deciding which constructor to use. Often classes have many builders, and it is very difficult to maintain them. It is common to see multiple constructors with different combinations of parameters.
What are the parts of the Builder design pattern?
We mentioned that the Builder design pattern proposes to create a complete object from a simpler one simplifying the creation of the object and helping us to obtain a consistent object.
We will then need a Builder object that will create the object based on parameters that we pass it step by step.
Usually and it is good practice to create an interface with a method build that will return the object we want.
We have then in this pattern:
The implementation of the builder interface that implements the method build and contains the other methods that receive the parameters needed to build the final object.
How to create the Builder design pattern in Java
We said that the purpose of the Builder pattern is to simplify the creation of objects that we consider complex. Let’s look at an example to understand how it works.
Imagine a class that keeps the details of a bank account.
package patterns.builder;
public class BankAccount {
private long accountNumber;
private String owner;
private BankAccountType type;
private double balance;
private double interestRate;
public BankAccount() {
}
public long getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(long accountNumber) {
this.accountNumber = accountNumber;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public BankAccountType getType() {
return type;
}
public void setType(BankAccountType type) {
this.type = type;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public double getInterestRate() {
return interestRate;
}
public void setInterestRate(double interestRate) {
this.interestRate = interestRate;
}
}
The builder interface is very simple, you just need a build method. Using the interface is optional. You could skip if you want and create the build directly, but it’s good practice to use the interface.
package patterns.builder;
public interface IBuilder {
BankAccount build();
}
Now you implement the interface and add the methods that will receive the parameters. In this example, all these methods that receive the parameters to create the object start with “with”. Each method returns the builder.
The builder method creates the object you need, using all parameters.
It is common to add any required field as an argument in the builder’s constructor leaving the remaining fields using the builder methods.
It is also common to see builders as internal static classes of the object you want to create.
package patterns.builder;
public class BankAccountBuilder implements IBuilder {
private long accountNumber; //This is important, so we will pass it to the constructor.
private String owner;
private BankAccountType type;
private double balance;
private double interestRate;
public BankAccountBuilder(long accountNumber) {
this.accountNumber = accountNumber;
}
public BankAccountBuilder withOwner(String owner){
this.owner = owner;
return this; //By returning the builder each time, we can create a fluent interface.
}
public BankAccountBuilder withType(BankAccountType type){
this.type = type;
return this;
}
public BankAccountBuilder withBalance(double balance){
this.balance = balance;
return this;
}
public BankAccountBuilder withRate(double interestRate){
this.interestRate = interestRate;
return this;
}
@Override
public BankAccount build(){
BankAccount account = new BankAccount();
account.setAccountNumber(this.accountNumber);
account.setOwner(this.owner);
account.setType(this.type);
account.setBalance(this.balance);
account.setInterestRate(this.interestRate);
return account;
}
}
Now we try the builder pattern with this example. First, you create the builder new BankAccountBuilder(accountNumber) which by default needs the account number because we have considered it an indispensable value. Then we use the builder, and we send the parameters' one by one.
package patterns.builder;
public class BuilderPatternExample {
public static void main(String[] args) {
BankAccountBuilder builder = new BankAccountBuilder(12345l);
BankAccount bankAccount = builder.withBalance(1000.20)
.withOwner("Oaken")
.withRate(10.15)
.withType(BankAccountType.PLATINUM)
.build();
System.out.println(bankAccount);
}
}
The output:
BankAccount{accountNumber=12345, owner='Oaken', type=PLATINUM, balance=1000.2, interestRate=10.15}
Some advantages and disadvantages of this pattern:
Code is easier to maintain when objects have a lot of attributes.
Decrease errors when creating the object because the builder specifies step by step how to create them and what attributes you need.
The biggest disadvantage is the need to maintain the duplication of attributes that must be in the target class and the builder.
Conclusion
When we work with complex objects or when complexity begins to grow, the Builder design pattern can separate this complexity by using another object to build the main object using a “step by step” to that end.
You can review this code GitHub