Builder Pattern
The Builder Pattern is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.
1. The Core Concept
Imagine a complex object that requires laborious, step-by-step initialization of many fields and nested objects. Such initialization code is usually buried inside a monstrous constructor with lots of parameters.
- The Problem: A constructor like
new House(windows, doors, rooms, hasGarage, hasSwimPool, hasStatues, hasGarden...). Most parameters will be unused or null. This is called the Telescoping Constructor Anti-Pattern. - The Solution: Extract the object construction code out of its own class and move it to separate objects called Builders.
2. Key Components
- Builder Interface: Declares product construction steps (e.g.,
buildWalls,buildRoof). - Concrete Builder: Implements the steps to construct a specific product (e.g.,
WoodHouseBuilder,StoneHouseBuilder). - Product: The complex object being built.
- Director (Optional): A class that executes the building steps in a specific sequence to create common configurations.
3. Example (TypeScript)
Let's build a custom Computer.
// The Product
class Computer {
public cpu: string = "";
public ram: string = "";
public storage: string = "";
public gpu: string = "";
public describe(): void {
console.log(`Computer: CPU=${this.cpu}, RAM=${this.ram}, Storage=${this.storage}, GPU=${this.gpu}`);
}
}
// The Builder Interface
interface ComputerBuilder {
setCPU(cpu: string): this;
setRAM(ram: string): this;
setStorage(storage: string): this;
setGPU(gpu: string): this;
build(): Computer;
}
// Concrete Builder
class GamingComputerBuilder implements ComputerBuilder {
private computer: Computer;
constructor() {
this.computer = new Computer();
}
setCPU(cpu: string): this {
this.computer.cpu = cpu;
return this; // Return this for method chaining
}
setRAM(ram: string): this {
this.computer.ram = ram;
return this;
}
setStorage(storage: string): this {
this.computer.storage = storage;
return this;
}
setGPU(gpu: string): this {
this.computer.gpu = gpu;
return this;
}
build(): Computer {
const result = this.computer;
this.computer = new Computer(); // Reset for next build
return result;
}
}
// Usage (Method Chaining / Fluent Interface)
const builder = new GamingComputerBuilder();
const myPC = builder
.setCPU("Intel i9")
.setRAM("32GB")
.setStorage("1TB SSD")
.setGPU("RTX 4090")
.build();
myPC.describe();
4. Pros and Cons
| Pros | Cons |
|---|---|
| Control: You have granular control over the construction process. | Complexity: Requires creating multiple new classes (Builder, ConcreteBuilder, Director). |
Readability: Fluent interfaces (.setA().setB()) are very readable compared to massive constructors. |
Coupling: The builder is tightly coupled to the specific product it builds. |
| Immutability: You can build the object step-by-step and only return the final immutable object at the end. |
5. Fluent Interface
A Fluent Interface is an implementation of an object-oriented API that aims to provide more readable code. Ideally, the code reads like a sentence in natural language.
In the context of the Builder pattern, this is achieved by Method Chaining.
- Mechanism: Each setter method in the builder returns the builder instance itself (
return this;). - Result: You can chain calls together:
builder.setA().setB().setC().
Without a fluent interface, the usage code would look like this:
builder.setCPU("Intel");
builder.setRAM("16GB");
builder.build();
programming/design-patterns programming/object-oriented-programming programming/factory-method-pattern