Mastering the Builder Design Pattern: Simplifying Complex Object Creation
Houda-DE
Posted on November 11, 2024
Introduction
All of us, as developers, have encountered situations where we need to create an object. The problem with trying to design a generic class for this object is that it can take multiple forms. A simple example: the User class of a platform. For a regular user, we might only need an email and a username. However, for a platform administrator, additional attributes like a phone number might be required. We could also have a Premium user, where an extra field like a credit card number is needed, and so on.
So, how can we proceed in a generic way?
Faced with this problem, the developer community agreed on a popular creation pattern: the Builder Design Pattern. This pattern involves separating the construction of a complex object from its representation, allowing multiple object variants to be created using the same construction process.
This pattern is especially useful with objects that have many attributes, some of which may be optional for certain cases but not for others, or for objects that require a detailed initialization process. It allows for the flexible, step-by-step creation of objects without making the code overly complex or the constructor too overloaded.
Sections We Will Cover
- What is the Builder Pattern?
- When you should use it?
- Real-World Examples of Builder Use
- Why this pattern is so important?
- Conclusion ## What is the Builder Pattern? The Builder Design Pattern is a creational design pattern that allows for the controlled and flexible construction of complex objects. Instead of having a constructor that takes a large number of parameters, the builder provides a smooth interface to create objects step by step. It is useful when the object has many attributes, specific construction steps, or multiple possible configurations.
When you should use it?
The Builder Design Pattern is particularly useful in the following cases:
If an object has many attributes, some of which are optional: Going back to the problem we posed at the beginning of this article, let’s consider the
User
class. Based on the logic we described, if we instantiate thisUser
class, we could have different cases: for a regular user,normalUser = new User("houda", "houda@gmail.com", null, null)
, for an adminadminUser = new User("houda", "houda@gmail.com", "0657...", null)
, and for a premium user,premiumUser = new User("houda", "houda@gmail.com", null, "1234...")
. This results in manynull
values in the instantiation.Objects with a multi-step creation process: An example is a
Order
class. The first step is to place the order, which is then prepared, and finally delivered. Preparing an order may require multiple steps, and to ensure the correct order of construction, the builder design pattern is very useful.Supporting multiple representations of the same object: For example, a
Clothing
class that has attributes for fabric, color, and brand. A clothing item can be aPants
,T-Shirt
, or other types. Here, the builder pattern helps create different representations of the same base class.
Real-World Examples of Builder Use
for each case we have seen in the section before we will see the implementation of the builder
- If an object has many attributes, some of which are optional:
class User {
username: string;
email: string;
phoneNumber?: string;
creditCard?: string;
private constructor(builder: UserBuilder) {
this.username = builder.username;
this.email = builder.email;
this.phoneNumber = builder.phoneNumber;
this.creditCard = builder.creditCard;
}
public static get Builder() {
return new UserBuilder();
}
}
class UserBuilder {
username!: string;
email!: string;
phoneNumber?: string;
creditCard?: string;
public setUsername(username: string): UserBuilder {
this.username = username;
return this;
}
public setEmail(email: string): UserBuilder {
this.email = email;
return this;
}
public setPhoneNumber(phoneNumber: string): UserBuilder {
this.phoneNumber = phoneNumber;
return this;
}
public setCreditCard(creditCard: string): UserBuilder {
this.creditCard = creditCard;
return this;
}
public build(): User {
return new User(this);
}
}
// Usage
const normalUser = User.Builder
.setUsername("houda")
.setEmail("houda@gmail.com")
.build();
const adminUser = User.Builder
.setUsername("houda")
.setEmail("houda@gmail.com")
.setPhoneNumber("0657....")
.build();
const premiumUser = User.Builder
.setUsername("houda")
.setEmail("houda@gmail.com")
.setCreditCard("1234....")
.build();
- Objects with a multi-step creation process:
class Order {
private state: string;
private constructor(builder: OrderBuilder) {
this.state = builder.state;
}
public static get Builder() {
return new OrderBuilder();
}
public getState(): string {
return this.state;
}
}
class OrderBuilder {
state: string = "Placed";
public prepareOrder(): OrderBuilder {
if (this.state === "Placed") {
this.state = "Prepared";
}
return this;
}
public deliverOrder(): OrderBuilder {
if (this.state === "Prepared") {
this.state = "Delivered";
}
return this;
}
public build(): Order {
return new Order(this);
}
}
// Usage
const completedOrder = Order.Builder
.prepareOrder()
.deliverOrder()
.build();
console.log(completedOrder.getState()); // "Delivered"
- Supporting multiple representations of the same object:
class Clothing {
type: string;
fabric: string;
color: string;
brand: string;
private constructor(builder: ClothingBuilder) {
this.type = builder.type;
this.fabric = builder.fabric;
this.color = builder.color;
this.brand = builder.brand;
}
public static get Builder() {
return new ClothingBuilder();
}
}
class ClothingBuilder {
type!: string;
fabric!: string;
color!: string;
brand!: string;
public setType(type: string): ClothingBuilder {
this.type = type;
return this;
}
public setFabric(fabric: string): ClothingBuilder {
this.fabric = fabric;
return this;
}
public setColor(color: string): ClothingBuilder {
this.color = color;
return this;
}
public setBrand(brand: string): ClothingBuilder {
this.brand = brand;
return this;
}
public build(): Clothing {
return new Clothing(this);
}
}
// Usage
const tShirt = Clothing.Builder
.setType("T-Shirt")
.setFabric("Cotton")
.setColor("Blue")
.setBrand("BrandA")
.build();
const pants = Clothing.Builder
.setType("Pants")
.setFabric("Denim")
.setColor("Black")
.setBrand("BrandB")
.build();
Why this pattern is so important?
The Builder Design Pattern is important for several key reasons, especially when it comes to managing the complexity of object creation. Here's why it's so valuable:
- Handling Complex Objects
When an object has many attributes, some of which may be optional or need to be set in a specific order, the Builder Pattern provides a clear and structured way to create the object.
- Improves Code Readability and Maintainability
By separating the object creation logic from the object itself, the Builder Pattern makes the code more readable and easier to maintain.
- Reduces Constructor Overloading
Instead of having multiple constructors with different combinations of parameters, the Builder Pattern eliminates the need for constructor overloading.
- Clear Separation of Concerns
The builder separates the construction of an object from its representation. This means that you can change how the object is constructed without affecting its representation, or vice versa.
Conclusion
The Builder Design Pattern is an essential tool for developers dealing with complex object creation. By breaking down the construction process into clear, manageable steps, it improves code readability, maintainability, and flexibility. Whether you're working with objects that have many attributes, require multi-step construction, or need to support multiple configurations, the Builder Pattern provides an elegant solution that prevents over-complicated constructors and reduces errors.
The blog covered:
- What is the Builder Pattern?
- When you should use it?
- Real-World Examples of Builder Use
- Why this pattern is so important?
Posted on November 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.