Builder Pattern

Ramkumar
4 min read5 days ago

Hello everyone! In this blog, we will be taking an in-depth look at the Builder Design Pattern.

Builder pattern

What is the Builder Pattern?

The Builder pattern is a creational design pattern that enables the flexible creation of complex objects. Don’t worry if you don’t understand everything right away. Let’s see the problems of typical object creation

Why Builder?

Problems

  1. Complex Object Creation: Managing Multiple Parameters
    Creating objects in programming often involves using constructors. However, constructors can become unmanageable when there are many parameters. This is known as the telescopic constructor anti-pattern. This anti-pattern is a code smell, making the class difficult to extend and maintain.
  2. Validation and Failing Object Creation
    In some cases, we need to validate parameters and prevent object creation if the parameters are invalid. This cannot be effectively done using traditional constructors.
  3. Immutability
    A mutable object is one whose state can change after its creation, whereas an immutable object’s state cannot change once it is created. Immutable objects are easier to maintain and are less prone to bugs compared to mutable objects, which can lead to unexpected issues.

HOW to implement?

  1. Naive Approach
    The Telescoping Constructor anti-pattern can be solved by using a HashMap. Instead of having a constructor with a long list of parameters, you can pass a HashMap to the constructor. This HashMap will contain all the parameters and their corresponding values. Here’s how you can implement it:
public class Database {
private String host;
private int port;
private String username;
private String password;

public Database(Map<String, String> config) {
if (config.containsKey("host")) {
this.host = config.get("host");
}
if (config.containsKey("port")) {
this.port = Integer.parseInt(config.get("port"));
}
if (config.containsKey("username")) {
this.username = config.get("username");
}
if (config.containsKey("password")) {
this.password = config.get("password");
}
}
}

The problem with this approach

  1. Type safety: A HashMap cannot have values with different types directly. To handle different types of values, we need to use a HashMap with String keys and Object values. However, this approach can lead to runtime errors if we try to cast the objects to the wrong types.
  2. Defined Parameter: With the above approach, identifying the parameters is difficult. We need to read the code to identify the parameters. This is not a good approach because it is difficult to maintain and extend the code.

Inner Class

Instead of using a hash map, we can use a class to accept parameters for object creation. The parameter class is type-safe, and it is easy to identify the parameters.

public class Database {
private String host;
private int port;
private String username;
private String password;

public Database(DatabaseParameters parameter) {
this.host = parameter.host;
this.port = parameter.port;
this.username = parameter.username;
this.password = parameter.password;
}
}

class DatabaseParameters {
public String host;
public int port;
public String username;
public String password;
}

The current approach requires creating a DatabaseParameters class separately and passing its instance to the Database constructor. This creates unnecessary complexity and violates the Open-Closed Principle because any changes or additions to parameters require modifications in both classes (Database and DatabaseParameters).

Solution:

To simplify and maintain type safety:

  1. Eliminate DatabaseParameters Class: Remove the separate class and directly integrate parameter handling within the Database class itself.
  2. Implement Inner Builder Class:
  • Define an inner Builder class inside Database to handle parameter configuration.
  • This Builder class will contain methods (host, port, username, password) to set parameters and a build() method to construct the Database object.

3. Include Validation Logic: Implement validation directly within the Builder class to ensure parameters are valid before constructing the Database object.

@Getter
public class Database {
private String name;
private String host;
private int port;
private DatabaseType type;

private Database() {
}

// Step 1 - Hide the constructor
private Database(String name, String host, int port, DatabaseType type) {
this.name = name;
this.host = host;
this.port = port;
this.type = type;
}

// Add a static method to get the builder object
public static DatabaseBuilder builder() {
return new DatabaseBuilder();
}

// Step 2 - Create a static inner class with same fields as the outer class
public static class DatabaseBuilder {
private Database database;

DatabaseBuilder() {
this.database = new Database();
}

// Step 3 - Create fluent interfaces for setter
public DatabaseBuilder withName(String name) {
this.database.name = name;
return this;
}

public DatabaseBuilder withUrl(String host, int port) {
this.database.host = host;
this.database.port = port;
return this;
}

public DatabaseBuilder mysql() {
this.database.type = DatabaseType.MYSQL;
return this;
}

// Step 4 - Create a build method to return the outer class object
public Database build() {

if (!isValid()) {
throw new IllegalArgumentException("Invalid database configuration");
}

return new Database(this.database.name, this.database.host, this.database.port,
this.database.type);
}

// Step 5 - Add a validation method
public Boolean isValid() {
if (this.database.name == null) {
return false;
}
return true;
}
}

}

Oh, yay! That’s it! We’ve successfully implemented the builder pattern.

Certainly! Here’s how a client can use the Database class after implementing the builder pattern

Database database = new Database.Builder()
.host("localhost")
.port(3306)
.username("admin")
.password("password")
.build();

Benefits:

  • Clear and Fluent API: The builder pattern provides a clear, readable, and fluent API for constructing objects. Method chaining (builder.method1().method2()) allows for intuitive configuration of object properties.
  • Improved Readability: By using descriptive method names (host(), port(), etc.) in the builder, code becomes more readable and self-explanatory compared to complex constructors with multiple parameters.
  • Flexibility in Object Creation: Builders allow varying configurations of objects without the need for multiple constructors or telescopic parameter lists. Clients can choose which parameters to set and in what order.
  • Immutable Objects: Builders can be designed to create immutable objects by setting parameters only once during object construction. This ensures thread safety and prevents unintended modifications after creation.
  • Encapsulation of Construction Logic: Builders encapsulate the object construction logic, including validation and complex initialization steps. This simplifies the client code that creates objects and centralizes logic that could otherwise be scattered.

--

--

Ramkumar

Passionate tech blogger diving into programming and emerging tech. Join me for insights and updates on the evolving world of software development!