Design Patterns: Builder
A creational design pattern that simplifies the process of creating complex objects by breaking it into smaller, more manageable steps.
What is the Builder Pattern?
A creational design pattern that simplifies the process of creating complex objects by breaking it into smaller, more manageable steps.
The Builder Pattern separates the construction of an object from its representation. Instead of using a massive constructor with a dozen parameters (some of which might be optional), you "build" the object step by step using methods that set specific parts of the object.
This pattern is perfect for:
Complex Objects: Objects that have many fields or configurations.
Readable Code: Avoiding long constructor arguments.
Customisation: Creating objects in a step-by-step manner based on specific needs.
Ok but why?
The Builder pattern comes with some benefits:
Improved Readability: The step-by-step approach makes the code easier to understand.
Flexibility: Builders allow you to create various configurations of an object without multiple constructors or messy parameter combinations.
Encapsulation: Keeps the construction logic separate from the object itself, adhering to the Single Responsibility Principle.
Consistency: Ensures a consistent process for creating objects, even if their structure is complex.
The Messy Way
Before I throw a code example of the Builder pattern into the article…
Lets check out an alternative ‘messy’ way of doing the same thing.
class SmartHome {
public function __construct(
public string $walls,
public string $roof,
public string $doors,
public array $smartDevices,
public bool $solarPanels = false,
public ?string $securitySystem = null,
public ?string $energyManagementSystem = null,
public ?string $garageType = null,
public ?string $poolType = null
) {}
}
// Creating a house with optional features
$smartHome = new SmartHome(
walls: "Concrete walls",
roof: "Solar roof",
doors: "Automated sliding doors",
smartDevices: ["Smart thermostat", "Smart lights", "Smart fridge"],
solarPanels: true,
securitySystem: "AI-based facial recognition",
energyManagementSystem: "Grid-tied battery storage",
garageType: "EV charging-enabled",
poolType: "Heated infinity pool"
);
While this works, it has several downsides:
Repetition Across the Codebase: When creating the object in multiple places, every instantiation requires duplicating the same extensive list of parameters. If the constructor changes, every single instantiation must be updated manually, leading to significant maintenance overhead.
Increased Risk of Errors: Passing a long list of parameters increases the likelihood of mistakes, such as misplacing or omitting arguments. Even small errors can lead to subtle bugs that are hard to trace.
Brittle Codebase: Tightly coupling the object creation logic to the constructor means that any change to the constructor’s parameter list—such as adding or reordering arguments—can break the code wherever the object is instantiated.
Limited Flexibility for Future Changes: If new features or configurations are added, they must be incorporated directly into the constructor. This creates a ripple effect, as all dependent code needs to be updated to account for the changes.
Inconsistent Instantiations: Without a unified way to construct the object, developers may implement slightly different configurations across the codebase. This inconsistency can lead to unexpected behaviour and complicates debugging.
Harder to Refactor: A constructor overloaded with logic makes refactoring daunting. Ideally, if the constructor changes, only one place should need updating. However, in this approach, every instance must be revisited.
Ideally, if we do ever need to change the constructor of this class, we only need to be doing in one place.
The Builder Pattern Way
By implementing the Builder Pattern, you encapsulate the object creation logic in one place. This means if you ever need to change how the object is constructed, you only need to update the builder, ensuring minimal disruption to the rest of your codebase.
This approach streamlines maintenance, reduces the risk of errors, and simplifies future enhancements.
Want an example? Lets get into it.
SmartHome
class SmartHome {
public function __construct(
public string $walls,
public string $roof,
public string $doors,
public array $smartDevices,
public bool $solarPanels = false,
public ?string $securitySystem = null,
public ?string $energyManagementSystem = null,
public ?string $garageType = null,
public ?string $poolType = null
) {}
}
First up, we’ll reuse the class above. The goal is use the Builder pattern to allow us to build the SmartHome object without directly passing everything into its constructor.
If we ever do need to change the constructor, we don’t need to go through the codebase searching, it’s centralised.
SmartHomeBuilder
class SmartHomeBuilder {
private string $walls;
private string $roof;
private string $doors;
private array $smartDevices = [];
private bool $solarPanels = false;
private ?string $securitySystem = null;
private ?string $energyManagementSystem = null;
private ?string $garageType = null;
private ?string $poolType = null;
private ?string $irrigationSystem = null;
private ?string $voiceAssistant = null;
private ?string $emergencyBackup = null;
public function setWalls(string $walls): self {
$this->walls = $walls;
return $this;
}
public function setRoof(string $roof): self {
$this->roof = $roof;
return $this;
}
public function setDoors(string $doors): self {
$this->doors = $doors;
return $this;
}
public function addSmartDevice(string $device): self {
$this->smartDevices[] = $device;
return $this;
}
public function setSecuritySystem(string $system): self {
$this->securitySystem = $system;
return $this;
}
public function setEnergyManagementSystem(string $system): self {
$this->energyManagementSystem = $system;
return $this;
}
public function enableSolarPanels(): self {
$this->solarPanels = true;
return $this;
}
public function setGarageType(string $type): self {
$this->garageType = $type;
return $this;
}
public function setPoolType(string $type): self {
$this->poolType = $type;
return $this;
}
public function setIrrigationSystem(string $system): self {
$this->irrigationSystem = $system;
return $this;
}
public function setVoiceAssistant(string $assistant): self {
$this->voiceAssistant = $assistant;
return $this;
}
public function setEmergencyBackup(string $backup): self {
$this->emergencyBackup = $backup;
return $this;
}
public function build(): SmartHome {
return new SmartHome(
$this->walls,
$this->roof,
$this->doors,
$this->smartDevices,
$this->securitySystem,
$this->energyManagementSystem,
$this->solarPanels,
$this->garageType,
$this->poolType,
$this->irrigationSystem,
$this->voiceAssistant,
$this->emergencyBackup
);
}
This code defines a SmartHomeBuilder
class that uses the Builder Pattern to construct a SmartHome
object.
You may notice this is quite similar to a Data Transfer Object. While a DTO is purely used for transferring data, the Builder class adds construction logic to assemble the final object. The Builder pattern is more dynamic because it enables step-by-step object creation, whereas a DTO is typically static and serves as a simple container for transporting structured data.
Lets put it altogether:
$builder = new SmartHomeBuilder();
$smartHome = $builder
->setWalls("Reinforced concrete walls")
->setRoof("Energy-efficient green roof")
->setDoors("Biometric access doors")
->addSmartDevice("Smart thermostat")
->addSmartDevice("Smart fridge")
->addSmartDevice("Smart security cameras")
->setSecuritySystem("AI-driven facial recognition")
->setEnergyManagementSystem("Off-grid solar battery storage")
->enableSolarPanels()
->setGarageType("EV charging-enabled garage")
->setPoolType("Heated infinity pool")
->setIrrigationSystem("AI-controlled drip irrigation")
->setVoiceAssistant("Integrated voice assistant")
->setEmergencyBackup("Gas generator with solar recharging")
->build();
Conclusion
The Builder Pattern is an excellent solution for managing the complexity of constructing objects with multiple attributes, especially when those objects have optional features or configurations. By using the SmartHomeBuilder
example, we can see how the Builder Pattern improves flexibility, readability, and maintainability of object creation.
It eliminates the need for unwieldy constructors with long parameter lists, reduces the risk of errors, and encapsulates the construction logic in one place.
Want a code example to play about with? Check out my code repository.