Mastering the Builder Design Pattern with Practical Examples

Nikolay Nikolov
6 min readOct 24, 2023

--

What Is It?

The Builder Design Pattern is a creational design pattern that facilitates the construction of complex objects by separating the construction process from the actual representation. It offers a systematic, step-by-step approach to building an object, allowing the creation of various representations using the same construction process.

Where is it used?

The Builder Pattern is employed in scenarios where object construction involves intricate configurations or complex initialization steps.

  • Complex Object Construction:
    — When dealing with objects that have numerous configuration options or require complex initialization steps, the Builder Pattern proves highly beneficial. It allows developers to encapsulate the construction logic within a dedicated builder, ensuring a more organized and flexible approach to creating objects.
  • Variability in Object Configuration:
    — The pattern is particularly useful when there is a need for flexibility in object configuration, and different configurations may result in distinct representations. By employing the Builder Pattern, developers can cater to diverse requirements without modifying the client code, promoting adaptability.

Advantages

The adoption of the Builder Pattern brings several advantages to the software development process:

  • Flexible Object Construction:
    — The Builder Pattern enables the construction of diverse objects using the same building process. This flexibility is crucial in scenarios where different configurations or variations of an object are required without the need for a multitude of constructor parameters.
  • Improved Readability and Maintainability:
    — By separating the construction steps into distinct methods within the builder, the code’s readability is significantly enhanced. Each method encapsulates a specific building aspect, making the overall construction process more comprehensible. This, in turn, improves maintainability, as modifications or additions to the construction process can be made in a modular and isolated manner.
  • Enhanced Configurability:
    — The pattern provides a convenient way to configure objects with numerous optional parameters. This is especially valuable when dealing with objects that may have a large number of configuration options, as it streamlines the configuration process and makes the code more maintainable.

Example

Consider the example of constructing a PersonalComputer object in PHP.

First, let’s examine an example of poorly constructed code:

<?php

class PersonalComputer
{
public function __construct(
private string $cpu,
private string $memory,
private string $storage
){}

public function display(): void {
echo sprintf("CPU: %s, Memory: %s, Storage: %s\n",
$this->cpu,
$this->memory,
$this->storage
);
}

// Add getters
}

interface PCManufacturerInterface {
public function create(): PersonalComputer;
}

class GamingPCManufacturer implements PCManufacturerInterface
{
public function create(): PersonalComputer {

return new PersonalComputer(
"AMD Ryzen 9",
"32 GB",
"1 TB NVMe SSD"
);
}
}

class OfficePCManufacturer implements PCManufacturerInterface
{
public function create(): PersonalComputer {

return new PersonalComputer(
"Intel Core i5",
"16 GB",
"500 GB HDD"
);
}
}

// Client code
$manufacturer = new GamingPCManufacturer();
$gamingPC = $manufacturer->create();

echo "Gaming PC created: ";
$gamingPC->display();


$manufacturer = new OfficePCManufacturer();
$officePC = $manufacturer->create();

echo "Office PC created: ";
$officePC->display();

Let’s delve into the issues present in the poorly constructed code example:

Lack of Encapsulation

  • The properties ($cpu, $memory, $storage) of the PersonalComputer class are public, violating the principle of encapsulation.
  • Direct access to properties can lead to unintended modifications and hinder future maintenance.

Limited Flexibility and Configurability

  • The PCManufacturer class directly assigns values to the PersonalComputer properties during construction, limiting the flexibility to create computers with different configurations.
  • This approach is not scalable for handling variations in computer specifications.

Poor Separation of Concerns

  • The construction and configuration logic are tightly coupled within the PCManufacturer class, leading to low cohesion and poor separation of concerns.
  • This makes it challenging to extend or modify the code without affecting other parts of the system.

Breach of the Open-Closed Principle

  • Any modifications to the structure of the PersonalComputer class, changes in the configuration logic within the PCManufacturer class, or the introduction of an additional type of personal computer may necessitate adjustments throughout the codebase.
  • This increases the risk of errors and makes maintenance cumbersome, violating the Open-Closed Principle.

To tackle these issues, adopting a design pattern such as the Builder Pattern can improve code organization, maintainability, and configurability.

class PersonalComputer 
{
public function __construct(
private string $cpu,
private string $memory,
private string $storage
){}

public function display(): void {
echo sprintf("CPU: %s, Memory: %s, Storage: %s\n",
$this->cpu,
$this->memory,
$this->storage
);
}

// Add getters
}

class PCBuilder
{
private string $cpu = "Default CPU";
private string $memory = "Default Memory";
private string $storage = "Default Storage";

public function setCPU(string $cpu): self {
$this->cpu = $cpu;

return $this;
}

public function setMemory(string $memory): self {
$this->memory = $memory;

return $this;
}

public function setStorage(string $storage): self {
$this->storage = $storage;

return $this;
}

public function build(): PersonalComputer {
return new PersonalComputer(
$this->cpu,
$this->memory,
$this->storage
);
}
}

// Abstract interface for the manufacturer
interface ComputerManufacturerInterface
{
public function createComputer(): PersonalComputer;
}

// Concrete implementation of the manufacturer for gaming computers
class GamingPCManufacturer implements ComputerManufacturerInterface
{
public function __construct(private PCBuilder $pcBuilder) {}

public function createComputer(): PersonalComputer
{
return $this->pcBuilder
->setCPU("AMD Ryzen 9")
->setMemory("32 GB")
->setStorage("1 TB NVMe SSD")
->build();
}
}

// Concrete implementation of the manufacturer for office computers
class OfficePCManufacturer implements ComputerManufacturerInterface
{
public function __construct(private PCBuilder $pcBuilder) {}

public function createComputer(): PersonalComputer
{
return $this->pcBuilder
->setCPU("Intel Core i5")
->setMemory("16 GB")
->setStorage("500 GB HDD")
->build();
}
}

// Client code
$pcBuilder = new PCBuilder();

// Gaming PC manufacturing
$gamingPCManufacturer = new GamingPCManufacturer($pcBuilder);
$gamingPC = $gamingPCManufacturer->createComputer();

echo "Gaming PC created: ";
$gamingPC->display();

// Office PC manufacturing
$officePCManufacturer = new OfficePCManufacturer($pcBuilder);
$officePC = $officePCManufacturer->createComputer();

echo "Office PC created: ";
$officePC->display();

The refactored implementation brings several benefits to the codebase:

1. Encapsulation and Immutability:
— The properties of the PersonalComputer class are private, enforcing encapsulation and immutability. Direct access to properties is avoided, reducing the risk of unintended modifications.

2. Readability and Maintainability:
— The code is more readable due to the use of a dedicated PCBuilder class for constructing PersonalComputer objects. Each method in the builder corresponds to a specific configuration option, improving code organization and making it easier to understand.
— The separation of concerns between the builder and the manufacturer enhances maintainability. Changes to the construction process can be made within the builder without affecting the manufacturer or the client code.

3. Configurability:
— The builder pattern allows for the creation of PersonalComputer objects with various configurations. The PCBuilder class provides a fluent interface for configuring individual components, making it easy to create different types of computers with custom specifications.

4. Flexibility:
— The introduction of the ComputerManufacturer interface allows for different manufacturers (e.g., GamingPCManufacturer, OfficePCManufacturer) to create computers using the same builder. This flexibility enables the addition of new computer types without modifying existing code, adhering to the Open-Closed Principle.

5. Centralized Construction Logic:
— The construction logic is centralized in the builder, promoting better separation of concerns. This not only improves code organization but also facilitates future modifications and extensions.

6. Code Reusability:
— The PCBuilder class can be reused for creating different types of computers. Manufacturers leverage the same builder with specific configurations, enhancing code reusability.

7. Adherence to Design Principles:
— The refactored code adheres to key design principles, including encapsulation, separation of concerns, and the Open-Closed Principle. This promotes a more robust and maintainable codebase.

In summary, the refactored implementation using the Builder Pattern improves code quality by enhancing readability, maintainability, and configurability, while also providing the flexibility to extend the system with new types of computers.

Feel free to follow me ➡️ or Subscribe 🔔 for more insightful content.
Clap 👏🏻, drop a comment 💬, and share this article with anyone you think would find it valuable.

Your interaction means the world to me and I’m incredibly grateful.
Your ongoing support gives me the power to write more valuable articles. ❤️ Thank you for being here and for reading!

Happy coding!

--

--

Nikolay Nikolov

Head of Software Development at CONUTI GmbH | 20 years experience | Passionate about clean code, design patterns, and software excellence.