Builder design pattern
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
The Builder pattern is a creational design pattern that allows you to create complex objects step-by-step. It separates the construction of an object from its representation, allowing you to create different representations of the same object. This pattern is used when the creation of an object is a complex process and involves multiple steps.
The pattern consists of a few key components:
The basic idea is that the client creates a Director object, which is responsible for constructing the complex object using a Concrete Builder object. The Concrete Builder object is responsible for constructing the parts of the complex object, and the Director object controls the order in which those parts are constructed.
UML Diagram:
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? |? ? ? Director? ? ?|
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? | -builder: Builder |
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? ? ? ? ? ?^
? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? |? ? ? Builder? ? ? |
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? | +buildPartA()? ? ?|
? ? ? ? ? ? ? ? ? | +buildPartB()? ? ?|
? ? ? ? ? ? ? ? ? | +buildPartC()? ? ?|
? ? ? ? ? ? ? ? ? | +getResult() : T? |
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? ? ? ? ? ?^
? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? |? ? ?Concrete? ? ? |
? ? ? ? ? ? ? ? ? |? ? ? Builder? ? ? |
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? | +buildPartA()? ? ?|
? ? ? ? ? ? ? ? ? | +buildPartB()? ? ?|
? ? ? ? ? ? ? ? ? | +buildPartC()? ? ?|
? ? ? ? ? ? ? ? ? | +getResult() : T? |
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? ? ? ? ? ?^
? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ?|
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? |? ? ? Product? ? ? |
? ? ? ? ? ? ? ? ? +-------------------+
? ? ? ? ? ? ? ? ? | +partA? ? ? ? ? ? |
? ? ? ? ? ? ? ? ? | +partB? ? ? ? ? ? |
? ? ? ? ? ? ? ? ? | +partC? ? ? ? ? ? |
? ? ? ? ? ? ? ? ? +-------------------+
In the above diagram, the Director is responsible for using the Builder to build a Product object. The Builder defines a set of methods for building the individual parts of the Product and a getResult() method for returning the final Product object. The ConcreteBuilder implements the Builder and provides its own implementation of the buildPartX() methods. The Product is the final object that is constructed by the Builder, and has its own set of parts.
What problem does builder pattern solve?
Consider a scenario where you need to create an object that has many properties, some of which are optional, and some of which are mandatory. In this case, you could create a constructor with many parameters, but this can quickly become difficult to manage as the number of parameters increases. Additionally, the constructor may become cluttered with conditional statements to handle optional parameters, making the code more complex and harder to understand.
How builder pattern solves?
The Builder pattern solves these problems by separating the construction of the object from its representation. Instead of using a single constructor with many parameters, you create a separate Builder class that takes care of the object's construction. The Builder class allows you to set the object's properties step-by-step, using a fluent interface or method chaining, and returns the final object when construction is complete.
Ideal Situation to Use builder pattern
The Builder pattern is ideal when you need to create complex objects that have many properties, some of which are optional. It's also useful when you want to create different representations of the same object without changing the client code. For example, imagine a user interface that allows users to create and customize their own sandwiches. The Builder pattern could be used to allow users to select the type of bread, meat, cheese, and toppings they want, and create a custom sandwich based on their selections.
Advantages
Limitations
Suppose you are building a complex object such as a computer, and it has many different parts that need to be assembled in a specific order. Without the Builder pattern, you might end up with a complex constructor that takes a large number of parameters:
class Computer {
public:
? ? Computer(string cpu, string motherboard, int ram, int storage,
bool hasWifi, bool hasBluetooth, bool hasTouchscreen);
? ? // ...
};
Creating an instance of this class would require passing in all of the required parameters, which could get messy and hard to read. Additionally, if you wanted to add or remove a feature such as Bluetooth or a touchscreen, you would need to modify the constructor and all the code that uses it.
The Builder pattern provides a solution to this problem by separating the construction of an object from its representation. Instead of requiring a complex constructor with many parameters, you define a builder class that is responsible for creating the object and setting its properties.
Here's an example of how the builder pattern can be used to create a computer:
class Computer {
public:
? ? void setCPU(string cpu);
? ? void setMotherboard(string motherboard);
? ? void setRAM(int ram);
? ? void setStorage(int storage);
? ? void setHasWifi(bool hasWifi);
? ? void setHasBluetooth(bool hasBluetooth);
? ? void setHasTouchscreen(bool hasTouchscreen);
? ? // ...
};
class ComputerBuilder {
public:
? ? void createNewComputer();
? ? void addCPU(string cpu);
? ? void addMotherboard(string motherboard);
? ? void addRAM(int ram);
? ? void addStorage(int storage);
? ? void addWifi();
? ? void addBluetooth();
? ? void addTouchscreen();
? ? Computer* getComputer();
private:
? ? Computer* m_computer;
};
The ComputerBuilder class has a createNewComputer() method that creates a new Computer object, and a number of methods for adding each of the required parts. You can then use the ComputerBuilder to add the required parts in any order, and create a new Computer object when you're done:
领英推荐
ComputerBuilder builder
builder.createNewComputer();
builder.addCPU("Intel Core i7");
builder.addMotherboard("ASUS P8Z77-V");
builder.addRAM(16);
builder.addStorage(500);
builder.addWifi();
builder.addBluetooth();
builder.addTouchscreen();
Computer* computer = builder.getComputer() ;
The builder pattern makes it easy to create complex objects without requiring a large number of parameters in a constructor. It also makes it easy to add or remove features from an object, since each feature has its own method in the builder class.
In terms of advantages, the builder pattern provides a clear separation between the construction of an object and its representation, which can make it easier to read and maintain code. It also allows for flexible creation of objects, since features can be added or removed as needed.
One potential limitation of the builder pattern is that it can be verbose, since each feature of an object requires its own method in the builder class. However, this is generally considered a small price to pay for the benefits of the pattern.
Below is an example of the creating the Burger object without using the Builder pattern:
#include <iostream>
#include <string>
class Burger {
public:
Burger(const std::string& bread, const std::string& meat,
const std::string& cheese, const std::string& vegetables)
: m_bread(bread), m_meat(meat), m_cheese(cheese), m_vegetables(vegetables) {}
void showBurger() {
std::cout << "Burger: " << m_bread << " with " << m_meat
<< " and " << m_cheese << " with " << m_vegetables << std::endl;
}
private:
std::string m_bread;
std::string m_meat;
std::string m_cheese;
std::string m_vegetables;
};
int main() {
Burger burger("Wheat", "Chicken", "Cheddar", "Lettuce, Tomato");
burger.showBurger();
return 0;
}
In the above code, we create a Burger class with a constructor that takes four parameters: bread, meat, cheese, and vegetables. We simply pass in the values for these properties when creating the Burger object, without the need for a separate builder object and set methods.
This approach works well for simple objects like the Burger in this example, where there are only a few properties, and the creation process is straightforward. However, for more complex objects with many properties or complex creation logic, the Builder pattern can be useful to make the creation process more organized and flexible.
After applying the Builder pattern for creating the Burger object:
#include <iostream>
#include <string>
class Burger {
public:
? void setBread(const std::string& bread) {
? ? m_bread = bread;
? }
? void setMeat(const std::string& meat) {
? ? m_meat = meat;
? }
? void setCheese(const std::string& cheese) {
? ? m_cheese = cheese;
? }
? void setVegetables(const std::string& vegetables) {
? ? m_vegetables = vegetables;
? }
? void showBurger() {
? ? std::cout << "Burger: " << m_bread << " with " << m_meat
? ? ? ? ? ? ? << " and " << m_cheese << " with " << m_vegetables << std::endl;
? }
private:
? std::string m_bread;
? std::string m_meat;
? std::string m_cheese;
? std::string m_vegetables;
};
class BurgerBuilder {
public:
? BurgerBuilder() {
? ? m_burger = new Burger();
? }
? ~BurgerBuilder() {
? ? delete m_burger;
? }
? void setBread(const std::string& bread) {
? ? m_burger->setBread(bread);
? }
? void setMeat(const std::string& meat) {
? ? m_burger->setMeat(meat);
? }
? void setCheese(const std::string& cheese) {
? ? m_burger->setCheese(cheese);
? }
? void setVegetables(const std::string& vegetables) {
? ? m_burger->setVegetables(vegetables);
? }
? Burger* getBurger() {
? ? return m_burger;
? }
private:
? Burger* m_burger;
};
int main() {
? BurgerBuilder burgerBuilder;
? burgerBuilder.setBread("Wheat");
? burgerBuilder.setMeat("Chicken");
? burgerBuilder.setCheese("Cheddar");
? burgerBuilder.setVegetables("Lettuce, Tomato");
? Burger* burger = burgerBuilder.getBurger();
? burger->showBurger();
? return 0;
}
As said earlier the Builder pattern allows the creation of complex objects to be simplified by separating the construction process from the object's representation. IIn this example, we have a Burger class that has four properties: bread, meat, cheese, and vegetables. We also have a BurgerBuilder class that allows us to build a Burger object in a step-by-step manner.
The advantage of using the Builder pattern in this case is that it allows us to create a Burger object in a step-by-step manner without having to remember the order in which the various properties should be set. It also makes it easy to reuse the same builder object to create multiple Burger objects with different properties.
Below is one more example to illustrate the usage of builder pattern:
Consider a simple application that generates an email. The email contains various components, such as the sender, recipient, subject, and body. We can use the Builder pattern to create an Email object by providing a Builder class that allows us to set the various components of the email in a step-by-step manner. Here's an example implementation:
#include <iostream>
#include <string>
class Email {
public:
? void setSender(const std::string& sender) {
? ? m_sender = sender;
? }
? void setRecipient(const std::string& recipient) {
? ? m_recipient = recipient;
? }
? void setSubject(const std::string& subject) {
? ? m_subject = subject;
? }
? void setBody(const std::string& body) {
? ? m_body = body;
? }
? void showEmail() {
? ? std::cout << "Sender: " << m_sender << std::endl;
? ? std::cout << "Recipient: " << m_recipient << std::endl;
? ? std::cout << "Subject: " << m_subject << std::endl;
? ? std::cout << "Body: " << m_body << std::endl;
? }
private:
? std::string m_sender;
? std::string m_recipient;
? std::string m_subject;
? std::string m_body;
};
class EmailBuilder {
public:
? EmailBuilder() {
? ? m_email = new Email();
? }
? ~EmailBuilder() {
? ? delete m_email;
? }
? void setSender(const std::string& sender) {
? ? m_email->setSender(sender);
? }
? void setRecipient(const std::string& recipient) {
? ? m_email->setRecipient(recipient);
? }
? void setSubject(const std::string& subject) {
? ? m_email->setSubject(subject);
? }
? void setBody(const std::string& body) {
? ? m_email->setBody(body);
? }
? Email* getEmail() {
? ? return m_email;
? }
? private:
? ? ? Email* m_email;
};
int main() {
? EmailBuilder emailBuilder;
? emailBuilder.setSender("[email protected]");
? emailBuilder.setRecipient("[email protected]");
? emailBuilder.setSubject("Meeting Tomorrow");
? emailBuilder.setBody("Hi Vinayak,\n\nJust a quick reminder that we have a meeting tomorrow at 10am.\n\nBest regards,\n Amit");
? Email* email = emailBuilder.getEmail();
? email->showEmail();
? return 0;
}?
/*
Op =>
amit@DESKTOP-9LTOFUP:~/OmPracticeC++$ ./a.out
Sender: [email protected]
Recipient: [email protected]
Subject: Meeting Tomorrow
Body: Hi Vinayak,
Just a quick reminder that we have a meeting tomorrow at 10am.
Best regards,
?Amit
*/
In this example, we have an Email class that has four properties: sender, recipient, subject, and body. We also have an EmailBuilder class that allows us to build an Email object in a step-by-step manner.
The advantage of using the Builder pattern in this case is that it allows us to create an Email object in a step-by-step manner without having to remember the order in which the various properties should be set. It also makes it easy to reuse the same builder object to create multiple Email objects with different properties.
Kotlin Example :
class User private constructor(
? ? val name: String,
? ? val email: String?,
? ? val age: Int?,
? ? val address: String?
) {
? ? class Builder(val name: String) {
? ? ? ? private var email: String? = null
? ? ? ? private var age: Int? = null
? ? ? ? private var address: String? = null
? ? ? ? fun setEmail(email: String?) = apply { this.email = email }
? ? ? ? fun setAge(age: Int?) = apply { this.age = age }
? ? ? ? fun setAddress(address: String?) = apply { this.address = address }
? ? ? ? fun build() = User(name, email, age, address)
? ? }
? ? override fun toString() = "User(name='$name', email=$email, age=$age, address=$address)"
}
In this example, we have a User class that has a private constructor and a nested Builder class. The Builder class allows clients to construct User objects in a more readable and maintainable way, by providing a fluent interface for setting the object's properties.
To use the Builder, clients can create a new instance of the Builder with the required properties, and then call the appropriate setter methods to set the optional properties. Finally, the build() method is called to create and return the User object.
Here's an example of how to use the Builder:
val user = User.Builder("Amit")
? ? .setEmail("[email protected]")
? ? .setAge(40)
? ? .setAddress("#75,Bangalore")
? ? .build()
println(user)
This creates a new User object with the name "Amit", an email of "[email protected]", an age of 40, and an address of "#75,Bangalore". The toString() method is called to print out the User object's properties.
Thanks for reading , please comment or suggest if any in comment section.