Introduction
The Builder Design Pattern is a creational design pattern used in software development. It allows for the step-by-step creation of immutable, complex objects using a simple and understandable interface. With the Builder Pattern, a builder object is used to create an object. The builder object has methods for setting different properties of the object it is creating and finally a method for returning the created object.
Let's consider we want to create a User class with a few mandatory fields like firstName
and age
along with a few non-mandatory fields like lastName
and country
. Using a constructor without setters for making immutable objects can be a viable option but it will become increasingly difficult to maintain as the number of parameters increases.
To handle this problem we can use the builder design pattern. The Builder design pattern provides an easy way to create immutable objects with mandatory and non-mandatory fields.
Builder Pattern Prior to Java 8
Steps:
Define a static inner class
UserBuilder
within the outer classUser
.In the outer class, create a private constructor so it can be instantiated only through the inner class.
The inner class should have the same fields as the outer class. It should have constructors to set mandatory fields and setters to set non-mandatory fields. Setters should return an instance of the inner class.
The inner class should also have a method (
build)
which uses fields from the inner class to return an instance of the outer class.
class User {
private String firstName;
private String lastName;
private String country;
private int age;
// private constructor so that User object can only be instantiated by UserBuilder
private User() {}
// Add Getters
@Override
public String toString() {
return "User{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", country='" + country
+ '\'' + ", age=" + age + '}';
}
public static class UserBuilder {
private String firstName;
private String lastName;
private String country;
private int age;
// Mandatory fields are provided in Constructor of builder
public UserBuilder(String firstName, int age) {
this.firstName = firstName;
this.age = age;
}
// Non mandatory fields are set using setters
public UserBuilder lastName(String lastName) {
this.lastName = lastName;
return this;
}
public UserBuilder country(String country) {
this.country = country;
return this;
}
// build method is used to create user object
public User build() {
User user = new User();
user.firstName = firstName;
user.lastName = lastName;
user.country = country;
user.age = age;
return user;
}
}
}
The client can use the User class as below:
public static void main(String[] args) {
User user = new User.UserBuilder("John", 20) // mandatory fields
.lastName("Doe") // non mandatory field
.country("USA") // non mandatory field
.build();
System.out.println("Created user: " + user);
User Bob = new User.UserBuilder("Bob", 21)
.lastName("Builder")
.build();
System.out.println("Created Bob: " + Bob);
}
Builder Pattern in Java 8
Java 8 comes with functional interfaces and lambda expressions. We can use Consumer
functional interface to take care of setting values in the Builder class.
Steps:
Define a static inner class
LambdaBuilder
within the outer classUser
.In the outer class, create a private constructor so it can be instantiated only through the inner class.
The inner class should have a method that accepts a Consumer of the inner class. This method is practically a replacement for the constructor and setters. The consumer takes care of setting all the fields in the inner class.
The inner class should also have a method (
build)
which uses fields from the inner class and creates an instance of the outer class.
class User {
private String firstName;
private String lastName;
private String country;
private int age;
// private constructor so that User object can only be instantiated by LambdaBuilder
private User() {}
// Getters
@Override
public String toString() {
return "User{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", country='" + country
+ '\'' + ", age=" + age + '}';
}
public static class LambdaBuilder {
public String firstName;
public String lastName;
public int age;
public String country;
// This method uses the provided consumer to set data in Builder
public LambdaBuilder with(Consumer<LambdaBuilder> builderFunction){
builderFunction.accept(this);
return this;
}
// build method is used to create User Object
public User build() {
User user = new User();
user.firstName = firstName;
user.lastName = lastName;
user.country = country;
user.age = age;
return user;
}
}
}
The client can use the User
class as below:
public class Main {
public static void main(String[] args) {
User charlie = new User.LambdaBuilder().with(user -> {
user.firstName = "Charlie";
user.lastName = "Hunnam";
user.country = "USA";
user.age = 30;
}).build();
System.out.println("Created Charlie: " + charlie);
}
}
Advantages
Allows for a more organized and efficient way of creating complex objects.
Makes code more maintainable and extensible by allowing for the creation of classes that can be easily modified and extended.
Makes code more reliable by ensuring that all the necessary components are included in the object.
Reduces the amount of code that needs to be written by allowing developers to reuse components.
Disadvantages
Can be difficult to debug if an incorrect combination of components is used.
Requires more effort to design and create the required classes.
Can be time-consuming to create all the necessary components.