The Builder design pattern is a creational pattern that allows for the step-by-step construction of complex objects. It separates the construction of an object from its representation, enabling the same construction process to create different representations. This pattern is particularly useful when an object has a complex structure or when the construction process should be independent of the parts that make up the object.
Example in TypeScript
Let’s consider an example where we want to build a House. A House can have various components like walls, doors, windows, and a roof. Using the Builder pattern, we can construct a House step-by-step.
Step 1: Define the Product
First, we define the House class, which is the complex object we want to build.
class House {
private walls: number;
private doors: number;
private windows: number;
private roof: string;
constructor() {
this.walls = 0;
this.doors = 0;
this.windows = 0;
this.roof = "";
}
setWalls(walls: number) {
this.walls = walls;
}
setDoors(doors: number) {
this.doors = doors;
}
setWindows(windows: number) {
this.windows = windows;
}
setRoof(roof: string) {
this.roof = roof;
}
showHouse() {
console.log(`House built with ${this.walls} walls, ${this.doors} doors, ${this.windows} windows, and a ${this.roof} roof.`);
}
}
Step 2: Define the Builder Interface
Next, we define the HouseBuilder interface, which declares the steps to build the House.
interface HouseBuilder {
buildWalls(walls: number): void;
buildDoors(doors: number): void;
buildWindows(windows: number): void;
buildRoof(roof: string): void;
getHouse(): House;
}
Step 3: Define the Concrete Builder
Then, we implement the HouseBuilder interface in a concrete builder class.
class ConcreteHouseBuilder implements HouseBuilder {
private house: House;
constructor() {
this.reset();
}
reset() {
this.house = new House();
}
buildWalls(walls: number): void {
this.house.setWalls(walls);
}
buildDoors(doors: number): void {
this.house.setDoors(doors);
}
buildWindows(windows: number): void {
this.house.setWindows(windows);
}
buildRoof(roof: string): void {
this.house.setRoof(roof);
}
getHouse(): House {
const result = this.house;
this.reset();
return result;
}
}
Step 4: Define the DirectorFinally, we define the Director class, which uses the HouseBuilder to construct the House.
class Director {
private builder: HouseBuilder;
setBuilder(builder: HouseBuilder): void {
this.builder = builder;
}
constructSimpleHouse() {
this.builder.buildWalls(4);
this.builder.buildDoors(2);
this.builder.buildWindows(4);
this.builder.buildRoof("Flat");
}
constructComplexHouse() {
this.builder.buildWalls(8);
this.builder.buildDoors(4);
this.builder.buildWindows(8);
this.builder.buildRoof("Gabled");
}
}
Step 5: Use the Builder Pattern
Now, we can use the Builder pattern to construct different types of houses.
const director = new Director();
const builder = new ConcreteHouseBuilder();
director.setBuilder(builder);
director.constructSimpleHouse();
const simpleHouse = builder.getHouse();
simpleHouse.showHouse();
director.constructComplexHouse();
const complexHouse = builder.getHouse();
complexHouse.showHouse();
Scenarios Where Builder Pattern Should Be Used
- Complex Object Construction: When an object has a complex structure and requires multiple steps to be constructed.
- Different Representations: When the same construction process can create different representations of an object.
- Step-by-Step Construction: When the construction process should allow for step-by-step creation of the object.
- Avoiding Telescoping Constructors: When a class has many optional parameters, leading to a telescoping constructor anti-pattern.
- Immutable Objects: When you want to create immutable objects but still need a flexible way to construct them.
By using the Builder pattern, you can simplify the construction of complex objects and make your code more maintainable and flexible.
Let create Immutable Objects using Builder Pattern.
Immutable Objects
The Builder pattern can be particularly useful for constructing immutable objects because it allows you to build the object step-by-step and then create the immutable object in a single step.
Let’s consider an example where we want to build an immutable Person object using the Builder pattern in TypeScript.
Step 1: Define the Immutable Product
First, we define the Person class, which is the immutable object we want to build.
class Person {
readonly firstName: string;
readonly lastName: string;
readonly age: number;
readonly address: string;
constructor(builder: PersonBuilder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.address = builder.address;
}
showPerson() {
console.log(`Person: ${this.firstName} ${this.lastName}, Age: ${this.age}, Address: ${this.address}`);
}
}
Step 2: Define the Builder Class
Next, we define the PersonBuilder class, which will be used to construct the Person object.
class PersonBuilder {
firstName: string = "";
lastName: string = "";
age: number = 0;
address: string = "";
setFirstName(firstName: string): PersonBuilder {
this.firstName = firstName;
return this;
}
setLastName(lastName: string): PersonBuilder {
this.lastName = lastName;
return this;
}
setAge(age: number): PersonBuilder {
this.age = age;
return this;
}
setAddress(address: string): PersonBuilder {
this.address = address;
return this;
}
build(): Person {
return new Person(this);
}
}
Step 3: Use the Builder Pattern
Now, we can use the PersonBuilder to construct an immutable Person object.
const personBuilder = new PersonBuilder();
const person = personBuilder
.setFirstName("John")
.setLastName("Doe")
.setAge(30)
.setAddress("123 Main St")
.build();
person.showPerson();
Explanation
- Immutable
PersonClass: ThePersonclass is immutable because its properties are declared asreadonly. This means that once aPersonobject is created, its properties cannot be changed. PersonBuilderClass: ThePersonBuilderclass allows you to set the properties of thePersonobject step-by-step. Thebuildmethod creates and returns a newPersonobject.- Fluent Interface: The
PersonBuilderclass uses a fluent interface, where each setter method returns the builder itself. This allows for method chaining, making the construction process more readable and concise.
By using the Builder pattern, you can construct immutable objects in a flexible and step-by-step manner, ensuring that the objects remain immutable after they are created.
Leave a Reply