Hello everyone! In this blog, we will be taking an in-depth look at the Builder Design 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
- 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. - 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. - 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?
- 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
- Type safety: A
HashMap
cannot have values with different types directly. To handle different types of values, we need to use aHashMap
withString
keys andObject
values. However, this approach can lead to runtime errors if we try to cast the objects to the wrong types. - 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:
- Eliminate
DatabaseParameters
Class: Remove the separate class and directly integrate parameter handling within theDatabase
class itself. - Implement Inner Builder Class:
- Define an inner
Builder
class insideDatabase
to handle parameter configuration. - This
Builder
class will contain methods (host
,port
,username
,password
) to set parameters and abuild()
method to construct theDatabase
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.