The Builder Design Pattern: Simplifying Object Construction

Tobias Lang
8 min readMay 20, 2023

Introduction

Design patterns play a crucial role in software development, providing reusable solutions to common design problems. One such pattern is the Builder design pattern¹, which focuses on creating complex objects step by step. In this article, we will delve into the Builder design pattern, examine its implementation in Python, discuss its pros and cons, and explore some examples of its usage in the Python standard library.

Design patterns are essential tools in software development, serving as reusable solutions to recurring design problems. Among these patterns, the Builder design pattern holds a significant position, specifically addressing the step-by-step creation of complex objects. By decoupling object construction from its representation, the Builder pattern enables the construction process to yield diverse representations using the same underlying steps. This pattern proves especially valuable when confronted with the challenges of building intricate objects, as it accommodates varying configurations and facilitates the execution of multiple construction steps. The Builder pattern empowers developers to efficiently construct complex objects, simplifying the design process and promoting code reusability.

The article is part of my series on Software Design Patterns. For a more general introduction to design patterns, refer to this overview article, which is an excellent starting point for exploring the fascinating world of software design patterns.

As usual, the sample code shown in the article can be found on my Github repository.

Photo by Scott Blake from Unsplash

Builder Design Pattern

In this pattern, the client delegates the construction process to a director, which uses a builder to construct the final product. The diagram illustrates the relationships between the client, director, builder, and product, showcasing the flow of interactions and responsibilities within the Builder pattern. This separation of concerns between the client, director, builder, and product allows for the flexible construction of complex objects, enabling different representations of the same construction process.

Builder design pattern UML
Trashtoy, Public domain, via Wikimedia Commons

In the Builder pattern, a client initiates the construction process and interacts with the Director to obtain the final product. The Director manages the construction process, receiving a Builder object from the client and instructing it to build the product step by step. The abstract Builder class defines the interface for building the product, while the ConcreteBuilder classes implement the Builder interface to provide specific implementations for building different parts of the product. The Product represents the complex object being constructed, with its structure and properties varying based on the specific implementation. By assigning the ConcreteBuilder to the Director, the client can guide the construction process, and once the product is complete, it can be obtained from the ConcreteBuilder.

Implementation

To implement the Builder design pattern, we typically define a Builder class that encapsulates the construction process. Let’s consider an example of building different cars using the Builder pattern:

""" Builder pattern module."""
from __future__ import annotations
from typing import Optional


class Director:
"""
Director class.
"""
def __init__(self) -> None:
"""
Initialize Director class.
"""
self.builder: Optional[Builder] = None

def set_builder(self, builder: Builder) -> None:
"""
Set the builder class.
"""
self.builder = builder

def construct_car(self, model: str, color: str, engine: str, wheels: int) -> None:
"""
Construct a car with the given parameters.
"""
if self.builder is None:
raise ValueError("Builder has not been set.")

self.builder.set_model(model)
self.builder.set_color(color)
self.builder.set_engine(engine)
self.builder.set_wheels(wheels)

def get_car(self) -> Car:
"""
Get the car that was built.
"""
if self.builder is None:
raise ValueError("Builder has not been set.")

return self.builder.get_car()

class Builder:
def __init__(self) -> None:
"""
Initialize Builder class with a car object (product).
"""
self.car = Car()

def set_model(self, model: str) -> None:
"""
Set the model of the car.
"""

def set_color(self, color: str) -> None:
"""
Set the color of the car.
"""

def set_engine(self, engine: str) -> None:
"""
Set the engine of the car.
"""

def set_wheels(self, wheels: int) -> None:
"""
Set the number of wheels of the car.
"""

def get_car(self) -> Car:
"""
Get the car that was built.
"""
return self.car

class ConcreteBuilderSportsCar(Builder):
"""
Concrete Builder class for a sports car.
"""
def set_model(self, model: str) -> None:
"""
Set the model of the car as a sports model.
"""
self.car.model = f"Sports {model}"

def set_color(self, color: str) -> None:
"""
Set the color of the car as a vibrant color.
"""
self.car.color = f"Vibrant {color}"

def set_engine(self, engine: str) -> None:
"""
Set the engine of the car as a powerful engine.
"""
self.car.engine = f"Powerful {engine}"

def set_wheels(self, wheels: int) -> None:
"""
Set the number of wheels of the sports car, more is better.
"""
self.car.wheels = wheels + 2


class ConcreteBuilderSUV(Builder):
"""
Concrete Builder class for an SUV.
"""
def set_model(self, model: str) -> None:
"""
Set the model of the car as an SUV model.
"""
self.car.model = f"{model} SUV"

def set_color(self, color: str) -> None:
"""
Set the color of the car.
"""
self.car.color = color

def set_engine(self, engine: str) -> None:
"""
Set the engine of the car.
"""
self.car.engine = engine

def set_wheels(self, wheels: int) -> None:
"""
Set the number of wheels of the car.
"""
self.car.wheels = wheels


class Car:
"""The Product class."""
def __init__(self) -> None:
"""
Initialize the Car class.
"""
self.model: Optional[str] = None
self.color: Optional[str]= None
self.engine: Optional[str] = None
self.wheels: Optional[int] = None

def __str__(self) -> str:
"""
String representation of the car.
"""
return f"Car: Model={self.model}, Color={self.color}, Engine={self.engine}, Wheels={self.wheels}"

# Usage
director = Director()

# Construct a sports car
sports_builder = ConcreteBuilderSportsCar()
director.set_builder(sports_builder)
director.construct_car("Sports Car", "Red", "V8", 4)
sports_car = director.get_car()
print(sports_car) # Car: Model=Sports Car, Color=Vibrant Red, Engine=Powerful V8, Wheels=6

# Construct an SUV
suv_builder = ConcreteBuilderSUV()
director.set_builder(suv_builder)
director.construct_car("SUV", "Blue", "V6", 4)
suv = director.get_car()
print(suv) # Car: Model=BMW SUV, Color=Blue, Engine=V6, Wheels=4

As we can see, adding new car models to the code will not touch the existing implementation. We just need to create additional ConcreteBuilder classes to expand the design.

Advantages and Disadvantages

The Builder design pattern offers several benefits. However, there are also some considerations to keep in mind.

Advantages

  • Encapsulation: The Builder pattern encapsulates the construction logic within the Builder class. This separation isolates the details of object creation from the client code, reducing dependencies and promoting cleaner code organization. The client delegates the construction process to the Builder, which abstracts away the complex construction logic.
  • Flexibility: Builders provide flexibility in configuring and constructing complex objects. By breaking down the construction process into step-by-step methods, builders allow clients to selectively invoke and customize the construction steps based on their specific needs. This flexibility enables the creation of objects with different variations and configurations without modifying the client code.
  • Readability: The Builder pattern enhances code readability by making the construction process explicit and intuitive. By utilizing descriptive method names within the Builder class, the steps involved in object creation become self-explanatory. This explicitness and clarity make the code easier to understand, maintain, and modify.
  • Reusability: Builders offer reusability by enabling the construction of multiple object variations using the same construction process. Once a builder is defined, it can be used to create different representations of the same object by changing the specific implementation of the builder’s methods. This reusability reduces code duplication and promotes a more modular and scalable approach to object creation.

Disadvantages

  • Increased complexity: While the Builder pattern provides benefits, it can introduce additional complexity, especially for simple object construction scenarios. The need for a separate Builder class and the step-by-step construction process may seem excessive when the object being built is straightforward and has minimal configuration options. In such cases, using the Builder pattern may overcomplicate the codebase and add unnecessary overhead.
  • Overhead: The Builder pattern can involve writing extra code to create the Builder and construct the object, which may be seen as unnecessary in certain situations. The presence of additional classes and methods for the construction process can increase the overall codebase size. In simpler cases where the object construction is straightforward and can be done directly, without the need for customization or complex configurations, using the Builder pattern may introduce unnecessary code verbosity and maintenance overhead.

Real-Life usage

The Builder design pattern can be found in various parts of the Python standard library.

One notable example is the email module, which provides a convenient way to construct email messages. The email.message.EmailMessage class serves as the Builder, allowing users to set various attributes such as the sender, recipient, subject, and message body before sending the email.

from email.message import EmailMessage

# Creating an email message using the Builder pattern
message = EmailMessage()
message.set_content("Hello, this is a test email.")
message["Subject"] = "Test Email"
message["From"] = "sender@example.com"
message["To"] = "recipient@example.com"
# Send the email using SMTP
# ...

Another example is datetime.datetime in the datetime module. The datetime.datetime class represents a date and time value. It provides a builder-like approach to create datetime objects using the datetime constructor and its various methods:

import datetime
# Building a datetime object using the builder pattern
dt = datetime.datetime(year=2023, month=5, day=14, hour=15, minute=30, second=0)
print(dt)

And finally sqlite3.Connection in the sqlite3 module. The sqlite3.Connection class represents a connection to an SQLite database and acts as the builder. The connect() method is used to create a database connection. Subsequent methods like execute() and commit() are used to configure and execute SQL statements:

import sqlite3
# Building a database connection using the builder pattern
connection = sqlite3.connect(":memory:")
cursor = connection.cursor()
cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES (?)", ("John Doe",))
connection.commit()

Conclusion

The Builder design pattern offers an elegant solution for constructing complex objects in a step-by-step manneSeparatinging the construction process from the object representation, it promotes code reusability, flexibility, and improved code readability. Although it may introduce some additional complexity and overhead, the benefits of the Builder pattern outweigh these considerations in scenarios where object construction involves multiple configurable steps.

In conclusion, the Builder design pattern is a valuable tool in the software developer’s arsenal, particularly when dealing with complex object construction. With its ability to encapsulate construction logic, provide flexibility, and enhance code readability, the Builder pattern proves to be a reliable approach to building objects in a step-by-step manner. By understanding and leveraging this pattern, developers can simplify the construction process and create more manageable and maintainable code.

Remember, design patterns are not strict rules but rather guidelines to help solve common design problems. It is crucial to evaluate the specific requirements and constraints of your project before deciding to implement a design pattern, including the Builder pattern.

¹ Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides

--

--