In this article, we’ll explore the Builder pattern in depth, including its definition, benefits, how it works and provide Java code examples to demonstrate how the pattern can be implemented.
When developing software, it’s common to encounter situations where we need to create objects that have many optional or mandatory attributes, or where there are many possible variations of the object. Creating constructors with many arguments to handle these situations can lead to code that is difficult to read and maintain, and can also make the code less flexible and extensible.
The Builder pattern is a creational design pattern that solves this problem by providing a way to construct complex objects step by step, by separating the object construction from its representation. This pattern is widely used in Java and other programming languages to create objects that have many configuration options and to avoid the complexity of creating multiple constructors or telescoping constructors.
Introduction
The Builder pattern is a creational design pattern that provides a way to construct complex objects step by step, by separating the object construction from its representation. This pattern was first described in the influential book “Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four.
The main idea behind the Builder pattern is to provide a flexible and extensible way to create objects that may have many optional or mandatory attributes, and to avoid the complexity of creating multiple constructors or telescoping constructors. The Builder pattern separates the construction of the object from its representation, allowing the same construction process to create different representations of the same object.
Wikipedia says
Builder pattern is an object creation software design pattern with the intentions of finding a solution to the telescoping constructor anti-pattern.
At one point or the other, we have all seen a constructor like below:
public class Car {
private String make;
private String model;
private int year;
private String color;
private String engine;
private String transmission;
private String fuelType;
private boolean hasGPS;
private boolean hasBluetooth;
private boolean hasBackupCamera;
private boolean hasHeatedSeats;
private boolean hasSunroof;
private boolean hasPremiumSoundSystem;
private boolean hasThirdRowSeating;
public Car(String make,
String model,
int year,
String color,
String engine,
String transmission,
String fuelType,
boolean hasGPS,
boolean hasBluetooth,
boolean hasBackupCamera,
boolean hasHeatedSeats,
boolean hasSunroof,
boolean hasPremiumSoundSystem,
boolean hasThirdRowSeating) {
this.make = make;
this.model = model;
this.year = year;
this.color = color;
this.engine = engine;
this.transmission = transmission;
this.fuelType = fuelType;
this.hasGPS = hasGPS;
this.hasBluetooth = hasBluetooth;
this.hasBackupCamera = hasBackupCamera;
this.hasHeatedSeats = hasHeatedSeats;
this.hasSunroof = hasSunroof;
this.hasPremiumSoundSystem = hasPremiumSoundSystem;
this.hasThirdRowSeating = hasThirdRowSeating;
}
// Getters and setters...
}
JavaIn this example, the Car
class has a long parameterized constructor that takes 14 arguments. While this constructor can be used to create instances of the Car
class, it can be difficult to read and maintain, especially if more attributes are added in the future.
In such cases, this pattern can be used to simplify object creation and make the code more readable and maintainable.
Implementation
In builder pattern, we define a Builder class which has a constructor to accept the required parameters and then has a method corresponding to each optional parameters each returning the instance of itself.
public class Car {
private String make;
private String model;
private int year;
private String color;
private String engine;
private String transmission;
private String fuelType;
private boolean hasGPS;
private boolean hasBluetooth;
private boolean hasBackupCamera;
private boolean hasHeatedSeats;
private boolean hasSunroof;
private boolean hasPremiumSoundSystem;
private boolean hasThirdRowSeating;
private Car(CarBuilder builder) {
this.make = builder.make;
this.model = builder.model;
this.year = builder.year;
this.color = builder.color;
this.engine = builder.engine;
this.transmission = builder.transmission;
this.fuelType = builder.fuelType;
this.hasGPS = builder.hasGPS;
this.hasBluetooth = builder.hasBluetooth;
this.hasBackupCamera = builder.hasBackupCamera;
this.hasHeatedSeats = builder.hasHeatedSeats;
this.hasSunroof = builder.hasSunroof;
this.hasPremiumSoundSystem = builder.hasPremiumSoundSystem;
this.hasThirdRowSeating = builder.hasThirdRowSeating;
}
// Getters...
public static class CarBuilder {
private String make;
private String model;
private int year;
private String color;
private String engine;
private String transmission;
private String fuelType;
private boolean hasGPS;
private boolean hasBluetooth;
private boolean hasBackupCamera;
private boolean hasHeatedSeats;
private boolean hasSunroof;
private boolean hasPremiumSoundSystem;
private boolean hasThirdRowSeating;
public CarBuilder(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public CarBuilder color(String color) {
this.color = color;
return this;
}
public CarBuilder engine(String engine) {
this.engine = engine;
return this;
}
public CarBuilder transmission(String transmission) {
this.transmission = transmission;
return this;
}
public CarBuilder fuelType(String fuelType) {
this.fuelType = fuelType;
return this;
}
public CarBuilder hasGPS(boolean hasGPS) {
this.hasGPS = hasGPS;
return this;
}
public CarBuilder hasBluetooth(boolean hasBluetooth) {
this.hasBluetooth = hasBluetooth;
return this;
}
public CarBuilder hasBackupCamera(boolean hasBackupCamera) {
this.hasBackupCamera = hasBackupCamera;
return this;
}
public CarBuilder hasHeatedSeats(boolean hasHeatedSeats) {
this.hasHeatedSeats = hasHeatedSeats;
return this;
}
public CarBuilder hasSunroof(boolean hasSunroof) {
this.hasSunroof = hasSunroof;
return this;
}
public CarBuilder hasPremiumSoundSystem(boolean hasPremiumSoundSystem) {
this.hasPremiumSoundSystem = hasPremiumSoundSystem;
return this;
}
public CarBuilder hasThirdRowSeating(boolean hasThirdRowSeating) {
this.hasThirdRowSeating = hasThirdRowSeating;
return this;
}
public Car build() {
return new Car(this);
}
}
}
JavaAnd this is how we can use it to create an instance of `Car` class-
Car car = new Car.CarBuilder("Toyota", "Camry", 2022)
.color("Red")
.engine("V6")
.transmission("Automatic")
.fuelType("Gasoline")
.hasGPS(true)
.hasBluetooth(true)
.hasBackupCamera(true)
.hasHeatedSeats(true)
.hasSunroof(false)
.hasPremiumSoundSystem(false)
.hasThirdRowSeating(false)
.build();
JavaSome common incorrect implementations of the Builder pattern
- The Builder class has multiple responsibilities: If the Builder class contains logic that should be part of the object being built, it violates the Single Responsibility Principle. The Builder class should only be responsible for constructing the object and not for performing any other unrelated operations.
- The Builder class is tightly coupled with the object being built: If the Builder class has knowledge of the internal state of the object or depends on specific implementation details of the object, it violates the Separation of Concerns principle. The Builder class should be decoupled from the object being built.
- The Builder class is not independent of the object being built: If the Builder class is not independent of the object being built, it cannot be used to build different types of objects. The Builder class should be independent of the object being built and should be able to construct different types of objects.
- The Builder class is modified every time a new type of object needs to be built: If the Builder class is modified every time a new type of object needs to be built, it violates the Open/Closed Principle. Instead of modifying the Builder class, a new builder class should be created that implements the same interface.
These incorrect implementations can lead to code that is difficult to maintain, test, and extend. To avoid these issues, it’s important to follow best practices when implementing the Builder pattern.
Usage in JVM
The Builder pattern is widely used in Java and the JVM ecosystem. Here are some examples of its usage:
StringBuilder
class:StringBuilder
is an implementation of the Builder pattern. It is used to construct strings efficiently by appending characters, substrings, or other objects.DateTimeFormatter
class:DateTimeFormatter
is a class in thejava.time
package that is used to format and parse dates and times. It provides a fluent API that follows the Builder pattern to create date and time formatters.Request
class in thejavax.servlet.http
package: TheRequest
class represents an HTTP request. It has a nested staticBuilder
class that is used to create instances of theRequest
class with various properties.ProcessBuilder
class:ProcessBuilder
is a class that is used to create and execute operating system processes. It provides a fluent API that follows the Builder pattern to create and configure process builders.
Lombok Builder
Lombok is a popular Java library that helps reduce boilerplate code in Java classes. It provides an easy way to generate boilerplate code, including builders, by adding annotations to your classes.
To use Lombok to generate builders, you need to add the @Builder
annotation to your class. Here’s an example:
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class Person {
private final String firstName;
private final String lastName;
private final int age;
private final String email;
}
JavaIn this example, we’ve added the @Builder
annotation to the Person
class. This will generate a nested static Builder
class with fluent setter methods for each field in the class.
To create an instance of Person
using the generated builder, you can use the following code:
Person person = Person.builder()
.firstName("John")
.lastName("Doe")
.age(30)
.email("john.doe@example.com")
.build();
JavaLombok’s @Builder
annotation can help reduce the amount of boilerplate code required to create builders in your Java classes, making your code more concise and readable.
Builder Anti-Pattern
The term “builder anti-pattern” is sometimes used to describe situations where the Builder pattern is misused or overused, leading to code that is more complex and less maintainable than it needs to be. Here are some common scenarios where the Builder pattern can become an anti-pattern:
- When there are only a few configuration options: If an object has only a few configuration options, creating a separate builder class may add unnecessary complexity to the code. In such cases, a simpler approach, such as a constructor or factory method, may be more appropriate.
- When the object is simple and doesn’t require a builder: If an object is simple and has only a few attributes, creating a builder class may be overkill. In such cases, a simpler approach, such as a constructor or factory method, may be more appropriate.
- When the builder is overly complex: If the builder class itself becomes overly complex, it can become difficult to read and maintain. In such cases, refactoring the builder class or using a simpler approach, such as a constructor or factory method, may be more appropriate.
- When there are too many variations of the object: If an object has too many possible variations, creating separate builder classes for each variation can lead to code duplication and make the code more complex. In such cases, a simpler approach, such as a factory method or abstract factory pattern, may be more appropriate.