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
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 this User class, we could have different cases: for a regular user, normalUser = new User("houda", "houda@gmail.com", null, null), for an admin adminUser = 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 many null 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 a Pants, T-Shirt, or other types. Here, the builder pattern helps create different representations of the same base class.
for each case we have seen in the section before we will see the implementation of the builder
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();
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"
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();
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:
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.
By separating the object creation logic from the object itself, the Builder Pattern makes the code more readable and easier to maintain.
Instead of having multiple constructors with different combinations of parameters, the Builder Pattern eliminates the need for constructor overloading.
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.
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:
The above is the detailed content of Mastering the Builder Design Pattern: Simplifying Complex Object Creation. For more information, please follow other related articles on the PHP Chinese website!