Introduction
If you've worked with frameworks like Mybatis, you're likely familiar with the following pattern: first creating a builder object, then calling the .build() method:
InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
This is an implementation of the Builder Pattern, which we'll explore in detail in this article.
What is the Builder Pattern?
The Builder Pattern is a creational design pattern that separates the construction of a complex object from its representation. This allows the same construction process to create different representations.
As a creational pattern, the Builder Pattern provides a flexible solution for constructing objects that require multiple steps or have many optional parameters. The key insight is that the construction process should be independent of the actual implementation of the components being built.
Structure and Implementation
The Builder Pattern consists of four main components:
- Product: The complex object being constructed
- Builder: An interface or abstract class that defines the methods for building the product's components and a method to retrieve the final product
- ConcreteBuilder: Implements the Builder interface to create a specific representation of the product
- Director: Constructs the complex object using the Builder interface, controlling the construction process
Example: Building Different Types of Vehicles
Let's consider a practical example where we need to construct different types of vehicles. Each vehicle has various components that can be configured differently.
First, let's define the Product class:
public class Vehicle {
private String engine;
private String wheels;
private String color;
private int seats;
private String entertainmentSystem;
// Getters and setters
public String getEngine() {
return engine;
}
public void setEngine(String engine) {
this.engine = engine;
}
// Other getters and setters...
@Override
public String toString() {
return "Vehicle{" +
"engine='" + engine + '\'' +
", wheels='" + wheels + '\'' +
", color='" + color + '\'' +
", seats=" + seats +
", entertainmentSystem='" + entertainmentSystem + '\'' +
'}';
}
}
Next, let's define the Builder interface:
public interface VehicleBuilder {
VehicleBuilder buildEngine(String engine);
VehicleBuilder buildWheels(String wheels);
VehicleBuilder buildColor(String color);
VehicleBuilder buildSeats(int seats);
VehicleBuilder buildEntertainmentSystem(String system);
Vehicle construct();
}
Now, let's create a ConcreteBuilder for a sports car:
public class SportsCarBuilder implements VehicleBuilder {
private Vehicle vehicle = new Vehicle();
@Override
public VehicleBuilder buildEngine(String engine) {
this.vehicle.setEngine(engine);
return this;
}
@Override
public VehicleBuilder buildWheels(String wheels) {
this.vehicle.setWheels(wheels);
return this;
}
@Override
public VehicleBuilder buildColor(String color) {
this.vehicle.setColor(color);
return this;
}
@Override
public VehicleBuilder buildSeats(int seats) {
this.vehicle.setSeats(seats);
return this;
}
@Override
public VehicleBuilder buildEntertainmentSystem(String system) {
this.vehicle.setEntertainmentSystem(system);
return this;
}
@Override
public Vehicle construct() {
// Additional validation specific to sports cars
if (this.vehicle.getSeats() > 2) {
throw new IllegalArgumentException("Sports cars typically have 2 seats");
}
return this.vehicle;
}
}
Let's create another ConcreteBuilder for an SUV:
public class SUVBuilder implements VehicleBuilder {
private Vehicle vehicle = new Vehicle();
@Override
public VehicleBuilder buildEngine(String engine) {
this.vehicle.setEngine(engine);
return this;
}
@Override
public VehicleBuilder buildWheels(String wheels) {
this.vehicle.setWheels(wheels);
return this;
}
@Override
public VehicleBuilder buildColor(String color) {
this.vehicle.setColor(color);
return this;
}
@Override
public VehicleBuilder buildSeats(int seats) {
this.vehicle.setSeats(seats);
return this;
}
@Override
public VehicleBuilder buildEntertainmentSystem(String system) {
this.vehicle.setEntertainmentSystem(system);
return this;
}
@Override
public Vehicle construct() {
// Additional validation specific to SUVs
if (this.vehicle.getSeats() < 5) {
throw new IllegalArgumentException("SUVs must have at least 5 seats");
}
return this.vehicle;
}
}
Now, let's create a Director class that orchestrates the construction process:
public class VehicleManufacturer {
private VehicleBuilder builder;
public VehicleManufacturer(VehicleBuilder builder) {
this.builder = builder;
}
public Vehicle manufactureVehicle() {
return builder.buildEngine("V6 Engine")
.buildWheels("All-season tires")
.buildColor("Black")
.buildSeats(5)
.buildEntertainmentSystem("Premium audio")
.construct();
}
}
Finally, let's see how to use these components:
public class VehicleDemo {
public static void main(String[] args) {
// Build a sports car
VehicleBuilder sportsCarBuilder = new SportsCarBuilder();
VehicleManufacturer sportsCarManufacturer = new VehicleManufacturer(sportsCarBuilder);
Vehicle sportsCar = sportsCarManufacturer.manufactureVehicle();
System.out.println("Sports Car: " + sportsCar);
// Build an SUV
VehicleBuilder suvBuilder = new SUVBuilder();
VehicleManufacturer suvManufacturer = new VehicleManufacturer(suvBuilder);
Vehicle suv = suvManufacturer.manufactureVehicle();
System.out.println("SUV: " + suv);
}
}
Simplified Implementation
In many cases, especially when the construction process is straightforward, we can simplify the implementation by using a static nested builder class:
public class Smartphone {
private final String os;
private final int ram;
private final int storage;
private final boolean hasCamera;
private final boolean has5G;
private Smartphone(Builder builder) {
this.os = builder.os;
this.ram = builder.ram;
this.storage = builder.storage;
this.hasCamera = builder.hasCamera;
this.has5G = builder.has5G;
}
// Getters...
public static class Builder {
private String os;
private int ram;
private int storage;
private boolean hasCamera = true; // Default value
private boolean has5G = false; // Default value
public Builder os(String os) {
this.os = os;
return this;
}
public Builder ram(int ram) {
this.ram = ram;
return this;
}
public Builder storage(int storage) {
this.storage = storage;
return this;
}
public Builder hasCamera(boolean hasCamera) {
this.hasCamera = hasCamera;
return this;
}
public Builder has5G(boolean has5G) {
this.has5G = has5G;
return this;
}
public Smartphone build() {
return new Smartphone(this);
}
}
}
Usage example:
Smartphone flagship = new Smartphone.Builder()
.os("Android 12")
.ram(12)
.storage(256)
.hasCamera(true)
.has5G(true)
.build();
Smartphone budget = new Smartphone.Builder()
.os("Android 10")
.ram(4)
.storage(64)
.build();
When to Use the Builder Pattern
The Builder Pattern is particularly useful in the following scenarios:
-
When an object needs to be created with many optional paraemters
-
When you want to avoid constructor overloading and telescoping constructors
-
When you need to create different representations of the same object
-
When construciton of an object involves multiple steps that need to be controlled The primary benefits of the Builder Pattern include:
-
Improved readability of the code by separating the construction process from the representation
-
Flexibility in creating different representations of the same object
-
Ability to enforce validation rules during construction
-
Reduction of constructor complexity and parameter explosion
However, there are some drawbacks to consider:
- Increased complexity if the pattern is overused for simple objects
- Additional classes and interfaces need to be maintained
- Can be less efficient than direct object creation for simple cases
Modern frameworks and libraries frequently use the Builder Pattern. For example, in gRPC, protocol buffer messages are often constructed using builders, providing a clean, readable way to create complex objects with many fields.