Design Patterns - Builder (in Java)
Definition
Do you ever feel tired, of setting so many properties during an object creation? One of the most used creational design patterns is the Builder Design Pattern provides flexibility and increases the readability of the object creation process.
Idea Behind Builder Design Pattern
Take help from a Builder class to build an object of
Class C
- Don’t use a constructor and setters in
class C
to set properties directly
- Don’t use a constructor and setters in
All the setters in Builder should return Builder for chaining
Towards the Builder Pattern - Step by Step
Suppose we have a Car class with lots of properties:
public class Car {
// Required parameters
private final String engine;
private final String transmission;
private final String bodyStyle;
// Optional parameters
private final String color;
private final String interior;
private final boolean sunroof;
private final boolean navigationSystem;
private final boolean parkingSensors;
private final boolean heatedSeats;
private final boolean bluetooth;
public Car(String engine, String transmission, String bodyStyle, String color, String interior,
boolean sunroof, boolean navigationSystem, boolean parkingSensors,
boolean heatedSeats, boolean bluetooth) {
this.engine = engine;
this.transmission = transmission;
this.bodyStyle = bodyStyle;
this.color = color;
this.interior = interior;
this.sunroof = sunroof;
this.navigationSystem = navigationSystem;
this.parkingSensors = parkingSensors;
this.heatedSeats = heatedSeats;
this.bluetooth = bluetooth;
}
@Override
public String toString() {
return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle +
", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof +
", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors +
", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car("V8", "Automatic", "SUV", "Red", "Leather", true, true, true, true, true);
System.out.println(car);
}
}
Just think about the Car constructor! Remembering all the parameter sequences we passed during object creation is hard!
The code loses readability.
It also forces us to pass the optional parameters, which are not mandatory.
Attempt 01: Overloaded Constructors
We may think of creating overloaded constructors with different combinations of optional parameters.
public Car(String engine, String transmission, String bodyStyle, String color) { this.engine = engine; // ... ... ... } public Car(String engine, String transmission, String bodyStyle, String color, String interior) { this.engine = engine; // ... ... ... } public Car(String engine, String transmission, String bodyStyle, String color, String interior, boolean bluetooth) { this.engine = engine; // ... ... ... } // ... ... ...
Don’t you think this is the worst design in history for creating an object? It is not easy to understand or remember what parameters we will pass and which constructor will be called! This will be a disaster!
Attempt 02: Using setter()
Methods
public class Car {
// Required parameters
private String engine;
private String transmission;
private String bodyStyle;
// Optional parameters
private String color;
private String interior;
private boolean sunroof;
private boolean navigationSystem;
private boolean parkingSensors;
private boolean heatedSeats;
private boolean bluetooth;
public Car(String engine, String transmission, String bodyStyle) {
this.engine = engine;
this.transmission = transmission;
this.bodyStyle = bodyStyle;
}
// Setters for optional parameters
public void setColor(String color) {
this.color = color;
}
public void setInterior(String interior) {
this.interior = interior;
}
public void setSunroof(boolean sunroof) {
this.sunroof = sunroof;
}
public void setNavigationSystem(boolean navigationSystem) {
this.navigationSystem = navigationSystem;
}
public void setParkingSensors(boolean parkingSensors) {
this.parkingSensors = parkingSensors;
}
public void setHeatedSeats(boolean heatedSeats) {
this.heatedSeats = heatedSeats;
}
public void setBluetooth(boolean bluetooth) {
this.bluetooth = bluetooth;
}
@Override
public String toString() {
return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle +
", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof +
", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors +
", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car("V8", "Automatic", "SUV");
car.setColor("Red");
car.setInterior("Leather");
car.setSunroof(true);
car.setNavigationSystem(true);
car.setParkingSensors(true);
car.setHeatedSeats(true);
car.setBluetooth(true);
System.out.println(car);
}
}
We can pass mandatory parameters in the constructor and keep setter methods to set the optional parameters if necessary.
But, If another thread tries to use the Car
object before all setters are called, it may see an incomplete object. So this approach is not thread-safe!
Attempt 03: Builder
is the Solution
public class Car {
// Required parameters
private final String engine;
private final String transmission;
private final String bodyStyle;
// Optional parameters
private final String color;
private final String interior;
private final boolean sunroof;
private final boolean navigationSystem;
private final boolean parkingSensors;
private final boolean heatedSeats;
private final boolean bluetooth;
private Car(Builder builder) {
this.engine = builder.engine;
this.transmission = builder.transmission;
this.bodyStyle = builder.bodyStyle;
this.color = builder.color;
this.interior = builder.interior;
this.sunroof = builder.sunroof;
this.navigationSystem = builder.navigationSystem;
this.parkingSensors = builder.parkingSensors;
this.heatedSeats = builder.heatedSeats;
this.bluetooth = builder.bluetooth;
}
@Override
public String toString() {
return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle +
", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof +
", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors +
", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
}
// Builder Class
public static class Builder {
// Required parameters
private final String engine;
private final String transmission;
private final String bodyStyle;
// Optional parameters
private String color;
private String interior;
private boolean sunroof;
private boolean navigationSystem;
private boolean parkingSensors;
private boolean heatedSeats;
private boolean bluetooth;
// All the mandatory parameters:
public Builder(String engine, String transmission, String bodyStyle) {
this.engine = engine;
this.transmission = transmission;
this.bodyStyle = bodyStyle;
}
public Builder color(String color) {
this.color = color;
return this;
}
public Builder interior(String interior) {
this.interior = interior;
return this;
}
public Builder sunroof(boolean sunroof) {
this.sunroof = sunroof;
return this;
}
public Builder navigationSystem(boolean navigationSystem) {
this.navigationSystem = navigationSystem;
return this;
}
public Builder parkingSensors(boolean parkingSensors) {
this.parkingSensors = parkingSensors;
return this;
}
public Builder heatedSeats(boolean heatedSeats) {
this.heatedSeats = heatedSeats;
return this;
}
public Builder bluetooth(boolean bluetooth) {
this.bluetooth = bluetooth;
return this;
}
public Car build() {
return new Car(this);
}
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car.Builder("V8", "Automatic", "SUV")
.color("Red")
.interior("Leather")
.sunroof(true)
.navigationSystem(true)
.parkingSensors(true)
.heatedSeats(true)
.bluetooth(true)
.build();
System.out.println(car);
}
}
Car
doesn’t expose the public constructors or settersBuilder
has the same number of properties,Car
hasBuilder
constructor receives all the mandatory propertiesTo set the optional properties
Builder
exposes public methods with Builder return type (this helps us chaining)finally
build()
method helps us to build theCar
object
The code becomes a bit complex but we can gain a lot of benefits from it.
Don’t Just Accept, Ask the Questions!
Let’s observe the builder pattern from a different angle.
What if we remove the helper Builder
class and implement the same functionality by the Car
class itself?
public class Car {
// Required parameters
private final String engine;
private final String transmission;
private final String bodyStyle;
// Optional parameters
private String color;
private String interior;
private boolean sunroof;
private boolean navigationSystem;
private boolean parkingSensors;
private boolean heatedSeats;
private boolean bluetooth;
public Car(String engine, String transmission, String bodyStyle) {
this.engine = engine;
this.transmission = transmission;
this.bodyStyle = bodyStyle;
}
public Car color(String color) {
this.color = color;
return this;
}
public Car interior(String interior) {
this.interior = interior;
return this;
}
public Car sunroof(boolean sunroof) {
this.sunroof = sunroof;
return this;
}
public Car navigationSystem(boolean navigationSystem) {
this.navigationSystem = navigationSystem;
return this;
}
public Car parkingSensors(boolean parkingSensors) {
this.parkingSensors = parkingSensors;
return this;
}
public Car heatedSeats(boolean heatedSeats) {
this.heatedSeats = heatedSeats;
return this;
}
@Override
public String toString() {
return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle +
", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof +
", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors +
", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
}
// all the getters if required ...
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car("V8", "Automatic", "SUV")
.color("Red")
.interior("Leather")
.sunroof(true)
.navigationSystem(true)
.parkingSensors(true)
.heatedSeats(true)
.bluetooth(true);
}
}
We can achieve the same functionality - right?
- Wrong!
Another characteristic of a Builder pattern is that it makes your object immutable. This means you can only set your properties during the object creation time; after that, you can’t update your object accidentally.
From the above example, myCar
can be updated again after the object is created!
Benefits of Builder Pattern
Improved readability and maintainability of our code
Flexible enough to create complex objects
The final object is immutable, helps prevent accidental changes
As the object creation does not involve multiple setters, it will not be in an inconsistent state
One Last Note
Be aware of using the Builder pattern, for objects with only a few properties or where all properties are mandatory, the Builder Pattern might not provide significant benefits. For example:
A Point
class with x
and y
coordinates might not need a builder.
Subscribe to my newsletter
Read articles from Shikhor Roy directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by