Table of Contents
Overview
The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates the construction of a complex object from its representation so that the same construction process can create different representations.
This pattern is particularly useful when an object needs to be created with many possible configurations. It provides a way to construct the object gradually by using a separate Builder class.
In the context of the Builder design pattern, the “construction” of an object refers to the process of creating and initializing an instance of a class. The “representation” of an object, on the other hand, refers to the final state of the object once it has been constructed.
When we say that the Builder pattern “separates the construction of an object from its representation”, we mean that the process of creating an object is decoupled from the specific state that the object is in once it’s created.
This separation allows the same construction process to create different representations. In other words, by changing the parameters we pass to the builder, we can create objects with different states using the same construction process.
Here’s a simple example to illustrate this:
Pizza margherita = new Pizza.Builder()
.setDough("thin")
.setSauce("tomato")
.setTopping("mozzarella")
.build();
Pizza hawaiian = new Pizza.Builder()
.setDough("thick")
.setSauce("tomato")
.setTopping("ham+pineapple")
.build();
In this example, we’re using the same Pizza.Builder
class to construct two different types of pizzas: Margherita and Hawaiian. The construction process is the same (i.e., we’re calling the same methods on the builder), but the representations of the objects (i.e., the final pizzas) are different because we’re passing different parameters to the builder.
House
example with the Builder pattern:
package design_pattern.creational.builder_design_pattern.demo1;
public class House {
private int floors;
private int rooms;
private boolean hasGarden;
private boolean hasPool;
private boolean hasGarage;
private House() {
// private constructor to enforce object creation through builder
}
public static class Builder {
private House house;
public static Builder newInstance() {
return new Builder();
}
public Builder() {
house = new House();
}
public Builder setFloors(int floors) {
house.floors = floors;
return this;
}
public Builder setRooms(int rooms) {
house.rooms = rooms;
return this;
}
public Builder setHasGarden(boolean hasGarden) {
house.hasGarden = hasGarden;
return this;
}
public Builder setHasPool(boolean hasPool) {
house.hasPool = hasPool;
return this;
}
public Builder setHasGarage(boolean hasGarage) {
house.hasGarage = hasGarage;
return this;
}
public House build() {
return house;
}
}
}
// Creating a House object with Builder pattern
public class Client {
public static void main(String[] args) {
House house = new House.Builder()
.setFloors(2)
.setRooms(3)
.setHasGarden(true)
.setHasPool(false)
.setHasGarage(true)
.build();
}
}
In this example:
- Construction refers to the process of creating a
House
object. This is done by calling variousset
methods on theBuilder
object (likesetFloors
,setRooms
, etc.), and then callingbuild
to finalize the construction. - Representation refers to the final
House
object that is created. This includes all the details of the house like the number of floors, number of rooms, whether it has a garden, a pool, a garage, etc.
The Builder pattern “separates the construction of an object from its representation” because the process of creating a House
object (i.e., calling set
methods on the Builder
) is separate from the final House
object that is created (i.e., the result of the build
method).
This separation allows the same construction process to create different representations. For example, you can use the same Builder
methods to create a House
object with 2 floors, 3 rooms, and a garden, or a House
object with 1 floor, 4 rooms, and a garage. The construction process is the same, but the final House
objects (i.e., the representations) are different.
Steps to Create BuilderPattern
Here’s an example of the Builder pattern in Java, demonstrating how to construct a complex Car
object:
Step 1: Define the Product Class
First, we define the Car
class, which is the complex object that we want to construct.
public class Car {
private String make;
private String model;
private int year;
private double engineCapacity;
// Constructor is private to force the use of the Builder
private Car(Builder builder) {
this.make = builder.make;
this.model = builder.model;
this.year = builder.year;
this.engineCapacity = builder.engineCapacity;
}
// Getters
public String getMake() { return make; }
public String getModel() { return model; }
public int getYear() { return year; }
public double getEngineCapacity() { return engineCapacity; }
// Static Builder class
public static class Builder {
private String make;
private String model;
private int year;
private double engineCapacity;
public Builder setMake(String make) {
this.make = make;
return this;
}
public Builder setModel(String model) {
this.model = model;
return this;
}
public Builder setYear(int year) {
this.year = year;
return this;
}
public Builder setEngineCapacity(double engineCapacity) {
this.engineCapacity = engineCapacity;
return this;
}
public Car build() {
return new Car(this);
}
}
}
Step 2: Use the Builder to Create an Object
The client code uses the Builder
class to create an instance of the Car
class.
public class BuilderPatternDemo {
public static void main(String[] args) {
Car car = new Car.Builder()
.setMake("Honda")
.setModel("Civic")
.setYear(2020)
.setEngineCapacity(1.8)
.build();
System.out.println("Car constructed: " + car.getMake() + " " + car.getModel());
}
}
In this example, the Car
class has a nested static Builder
class with methods to set the properties of the Car
. Each method returns the Builder
object itself, allowing for method chaining. The build()
method constructs the Car
instance with the current state of the builder.
This pattern allows for clear separation between the construction and representation of an object, enables code reuse with an object-oriented way to construct complex objects, and makes it easy to introduce new parameters to the object construction without changing all of the places where the object is created.
Why Builder Methods Should Return the Builder Instance 🏗️
In Java, the Builder pattern is a popular design pattern for constructing complex objects. It provides a clear and flexible way to construct an object step by step, and it’s especially useful when an object has many parameters. 📚
The Problem with Void Setter Methods ❌
Consider the following Builder class:
public class Builder{
private String attribute;
public void setAttribute(String attribute) {
this.attribute = attribute;
}
public Object build(){
return new Object(this);
}
}
In this case, the setAttribute
method is void, meaning it doesn’t return anything. If you want to set the attribute and then build the object, you’d have to do it in two steps:
Builder builder = new Builder();
builder.setAttribute("value");
Object object = builder.build();
This works, but it’s not very elegant or readable, especially if you have many attributes to set. 😕
The Solution: Return the Builder Instance ✔️
Now, let’s change the setAttribute
method so that it returns the Builder instance:
public class Builder{
private String attribute;
public Builder setAttribute(String attribute) {
this.attribute = attribute;
return this;
}
public Object build(){
return new Object(this);
}
}
With this change, you can now chain the method calls together:
Object object = new Builder().setAttribute("value").build();
This is much cleaner and more readable. It’s also more flexible, as you can easily add more attributes to set without making the code more complex. 🎉
Why Builder class is made of static?
The Builder class is made static inside the host class (e.g., ‘Car’ in the provided example) for several reasons:
1. Independence from the Outer Class Instance: A static nested class does not require an instance of the outer class to be created. This is particularly useful for the Builder pattern because you often want to create an instance of the product class (‘Car’ in this example) without having an existing instance of the outer class. Making the Builder class static allows it to be instantiated independently of the outer class.
2. Encapsulation: By defining the Builder class as a static nested class within the product class, it is kept within the namespace of the product class, which is a good encapsulation practice. It logically groups the Builder class with its associated product class without requiring an instance of the product class to exist.
3. Access to Private Members of the Outer Class: A static nested class has access to the private members (fields, constructors, and methods) of the outer class. This is beneficial for the Builder pattern, as the Builder often needs to access private constructors or fields of the product class to construct an instance. This access allows the Builder to fully manage the instantiation process, including setting values for private fields without exposing these fields directly through public setters.
4. Simplifying the Client Code: Since the Builder is a static nested class, the client code can instantiate it without first having to instantiate the outer class. This makes the client code cleaner and more straightforward, as it can directly call `new Car.Builder()` without needing an instance of `Car`.
5. Clarity and Readability: Making the Builder class static and nesting it within the product class helps to clarify that this Builder is specifically for constructing instances of the outer class. It organizes the code in a way that makes the relationship between the Builder and the product class clear, improving the readability and maintainability of the code.
In summary, making the Builder class static and nesting it within the product class leverages several advantages in terms of design and usability, making it a common practice in implementing the Builder pattern in Java.
Why does the Product class have a private constructor?
In the Builder Design Pattern, the Car
class has a private constructor to enforce the use of the Builder
class for creating Car
objects. Here’s why:
- Encapsulation: The private constructor ensures that the
Car
class’s fields can only be set through theBuilder
. This encapsulates the construction logic within theBuilder
and keeps theCar
class simple and focused on its primary responsibilities. - Flexibility: The
Builder
allows for flexible and readable object creation. You can set only the fields you’re interested in and leave the rest with default values. This wouldn’t be possible with a public constructor without ending up with a large number of constructor overloads. - Immutability: Once a
Car
object is created, it can’t be changed. This is a desirable property in multi-threaded environments where mutable objects can lead to issues. - Robustness: The
build()
method in theBuilder
can check the validity of the parameters before creating the object. This ensures that theCar
object is always in a valid state.
Understanding the Benefits of the Builder Design Pattern
Here are some scenarios where the Builder pattern can be useful:
- Complex Objects: The Builder design pattern is a good choice when you’re working with complex objects that should be created in a step-by-step fashion. It’s especially useful when an object requires many parameters in its constructor.
- Immutable Objects: The Builder design pattern can be used to construct immutable objects. Once the object is constructed, it cannot be changed. This is useful in multi-threaded environments where mutable objects can lead to issues.
- Configurable Objects: If an object has many configuration options, the Builder pattern can make it easier to create instances of that object. Instead of having to remember the order of parameters in a constructor, you can just specify the name of the configuration option.
- Readability: The Builder pattern can improve the readability of your code. By using method chaining (a series of methods that return the builder object), you can make your code more intuitive and easy to understand.