Separate the construction of a complex object from its representation so that the same construction process can create different representations
We will often have classes which require a large number of parameters or which have a large number of optional parameters. It may be difficult to handle these cases with a single constructor.
In other cases, we may have classes with inherently complex structures, like trees, graphs, etc.
Let’s assume that we have to have some contact information for a Customer, but we don’t care what kind.
public class Customer {
private final String userName;
private final String phoneNumber;
private final String email:
}
In other words, we must have at least a phone number or an email, but do not require both.
One solution would be to provide constructors for all possibilities:
public Customer(final String userName, final String phoneNumber){}
public Customer(final String userName, final String email){}
public Customer(final String userName,
final String phoneNumber,
final String email){}
Note: we could use the third constructor, but passing in null values, but that is a bad design!
This leads to a poor user experience when the number of parameters and the conditions grows, as we have to create a very large number of constructors that the user must sift through.
Alternatively, we could use a Builder:
public class Builder {
private String user=null;
private String phone=null;
private String email=null;
public Builder setUserName(String user){this.user=user;}
public Builder setPhone(String phone){this.phone=phone;}
public Builder setEmail(String email){this.email=email;}
public Customer build(){
if(user!=null && (phone!=null || email!=null)){
return Customer(user,phone,email);
}else{
throw new IllegalArgumentException("Invalid parameters!");
}
}
}
Now we can create a Customer in a more intuitive way:
final Customer customer =
new Builder()
.setUserName("pwright4")
.setEmail("pwright4@murraystate.edu")
.build();
We don’t have to find the appropriate constructor and order for the parameters, we just call the obvious methods for the data we have. If we don’t have the data to create a valid instance, we get an exception.
This already leads to some other problems, though:
We can solve the first problem with a little more work and using strong static typing:
public class Builder{
public BuilderWithUser setUser(String user){
return new BuilderWithName(user);
}
}
public class BuilderWithUser{
private final String user;
public BuilderWithUser(String user){
this.user = user;
}
public BuilderWithContact setEmail(String email){
return new BuilderWithEmail(user,email,null);
}
public BuilderWithContact setPhone(String phone){
return new BuilderWithPhone(user,null,phone);
}
}
public class BuilderWithContact {
private final String user;
private final String email;
private final String phone;
// ...
public BuilderWithContact setEmail(String email){
return new BuilderWithContact(user,email,phone);
}
public BuilderWithContact setPhone(String phone){
return new BuilderWithContact(user,email,phone);
}
public Customer build(){return new Customer(user, email, phone);}
}
Now we have obtained protection from invalid objects by relying on the types to ensure that we can only call build when we have the right data.
However, this enforces an order on the calls and also may not scale well.
The other issue we had was that we’re still exposing a constructor in Customer that accepts all parameters where we are expected to allow null values.
We typically limit the scope of this problem by nesting the Builder inside of the class being built (or use package privacy, etc.) so that we don’t expose the constructor to general users.
public class Customer{
// ...
private Customer(String user, String email, String phone){
//only the Builder can access this constructor!
}
public static class Builder{
//...
}
}
Another benefit of using the Builder pattern is that it allows us to separate the actual instantiation of an instance from the code that needs it, similar to how factory methods and abstract factories worked.
The end user provides the data, but has no control over the actual instance that is created.
Also, note that the builders are objects themselves that can be passed around in your code, so you don’t have to build your instance immediately. You could, for example, provide some data, then pass the builder to another method which will add more data, etc. then only build when you need the actual result.