Creational Pattern Series | Builder

Ali Mohammad
10 min readApr 26, 2024

In this series we will take a look at the creational design patterns:

  1. Singleton
  2. Factory Method
  3. Abstract Factory
  4. Prototype
  5. Builder

🧐 What it is ?

The builder enables you to gradually construct complex items. The pattern enables you to create many types and representations of an object using the same creation code.
Also, it is used to distinguish between an object’s actual construction and its representation. This makes it simpler to build complicated items piece by piece and also makes it simpler to alter an object’s representation without changing its construction.

There are two types of this design pattern:

  1. Classic Builder Pattern: This type of Builder Pattern involves creating a separate builder class that is responsible for creating and configuring a complex object. The builder class abstracts the object creation process from the client code, allowing it to focus on the logic of the program.
  2. Fluent Builder Pattern: This type of Builder Pattern is an extension of the classic Builder Pattern. It allows for the creation of a complex object in a more fluent and expressive manner, using method chaining to set multiple properties of an object in a single line of code. This can make the code more readable and intuitive to understand

The classic builder is best for making complex objects with many properties that can be changed, while the fluent builder is better for making simple objects with fewer properties that can be set in a clear and concise way.

🥺 The why ?

Let’s say we have a game where the player can create their own custom weapon by choosing different parts, such as the hilt, blade, and enchantment. Each part has its own set of properties that need to be set in order to create a fully functional weapon, and there may be many possible combinations of parts. This is where the Builder pattern can come in handy.

We can create a Weapon class with properties like name, damage, durability, and enchantment, and then create a WeaponBuilder class that implements the Builder interface to build Weapon objects. The WeaponBuilder class can have methods for setting the various properties of the weapon, and it can ensure that the properties are set in the correct order.

Let’s see what will it be like without the Builder:

class Weapon {
constructor(
public name: string,
public damage: number,
public durability: number,
public enchantment: string) {}
}
// Usage example
const weapon = new Weapon("Sword of Fire", 50, 100, "Fire");
console.log(weapon);

Here, we’re creating a new Weapon object directly in the Weapon class constructor. While this works fine for a simple example like this, it can quickly become unwieldy for more complex objects with many properties.

If we were to add more properties to the Weapon class, or if we had more complex logic for setting the properties, the constructor would become more and more difficult to use and maintain. It would also become difficult to ensure that the properties are set in the correct order and with the correct values.

Additionally, if we had many possible combinations of properties, we would need to create many different constructor overloads or factory methods to handle all of the different combinations. This could quickly become unmanageable and result in code duplication.

Fortunately, the Builder pattern solves these problems by separating the construction of the object from its representation, allowing for more flexibility and easier maintenance of the code. By providing a separate Builder class with methods for setting the properties in a specific order, we can ensure that the properties are set correctly and avoid code duplication.

🥳 The structure:

  1. The procedures for creating a product that are standard to all builders are declared in the Builder interface.
  2. The construction process is implemented in many ways by Concrete Builders. Products from concrete contractors could not adhere to the standard interface.
  3. (Optional) The Director class defines the order in which to call construction steps, so you can create and reuse specific configurations of products.
    In the Builder design pattern, the Director is an optional component that controls the order in which the building blocks of a complex object are assembled. The Director acts as a mediator between the client and the Builder, and it specifies the steps involved in the creation of the final object.
    The Director is not always necessary, but it can be useful when there is a complex object with many building blocks that need to be assembled in a specific order. By using a Director, the client code does not need to know the details of how the object is being built, but can simply provide the necessary input to the Director and let it handle the creation of the object.
    The Director works with a Builder interface, which defines the methods for building each part of the object. The Builder interface provides a way for the Director to work with any Builder object that implements the interface, allowing for greater flexibility in object creation.
interface Builder {
buildPartA(): void;
buildPartB(): void;
buildPartC(): void;
}
class ConcreteBuilder implements Builder {
buildPartA() {
console.log('Building part A');
}
buildPartB() {
console.log('Building part B');
}
buildPartC() {
console.log('Building part C');
}
}
class Director {
private builder: Builder;
constructor(builder: Builder) {
this.builder = builder;
}
construct() {
this.builder.buildPartA();
this.builder.buildPartB();
this.builder.buildPartC();
}
}
// Usage example
const builder = new ConcreteBuilder();
const director = new Director(builder);
director.construct();

The Classic & Fluent

😎 Lets see an example:

We will create the weapon that we talked about earlier:

class Weapon {
constructor(public name: string, public damage: number, public durability: number, public enchantment: string) {}
}
interface Builder {
setName(name: string): void;
setDamage(damage: number): void;
setDurability(durability: number): void;
setEnchantment(enchantment: string): void;
build(): Weapon;
}
class WeaponBuilder implements Builder {
private name: string = "";
private damage: number = 0;
private durability: number = 0;
private enchantment: string = "";
setName(name: string) {
this.name = name;
}
setDamage(damage: number) {
this.damage = damage;
}
setDurability(durability: number) {
this.durability = durability;
}
setEnchantment(enchantment: string) {
this.enchantment = enchantment;
}
build() {
return new Weapon(this.name, this.damage, this.durability, this.enchantment);
}
}
// Usage example
const builder = new WeaponBuilder();
builder.setName("Sword of Fire");
builder.setDamage(50);
builder.setDurability(100);
builder.setEnchantment("Fire");
const weapon = builder.build();
console.log(weapon);

Now, we have a Weapon class that represents a custom weapon in the game, and a WeaponBuilder class that implements the Builder interface and provides the necessary methods for building Weapon objects. We then create a new WeaponBuilder and use its methods to set the various properties of the weapon. Finally, we call the build() method on the WeaponBuilder to create a new Weapon object with the specified properties and log it to the console.

Using the Builder pattern in this way makes it easy to create custom weapons with different combinations of properties, and ensures that the properties are set in the correct order. It also allows for more flexibility in the construction of the Weapon object, and makes the code easier to maintain and extend.

We can take this even further by introducing the Director:

class Weapon {
constructor(public name: string, public damage: number, public durability: number, public enchantment: string) {}
}
interface Builder {
setName(name: string): void;
setDamage(damage: number): void;
setDurability(durability: number): void;
setEnchantment(enchantment: string): void;
getResult(): Weapon;
}
class BasicSwordBuilder implements Builder {
private weapon: Weapon;
constructor() {
this.weapon = new Weapon("Basic Sword", 10, 50, "");
}
setName(name: string) {
this.weapon.name = name;
}
setDamage(damage: number) {
this.weapon.damage = damage;
}
setDurability(durability: number) {
this.weapon.durability = durability;
}
setEnchantment(enchantment: string) {
this.weapon.enchantment = enchantment;
}
getResult() {
return this.weapon;
}
}
class FireSwordBuilder implements Builder {
private weapon: Weapon;
constructor() {
this.weapon = new Weapon("Fire Sword", 20, 40, "Fire");
}
setName(name: string) {
this.weapon.name = name;
}
setDamage(damage: number) {
this.weapon.damage = damage;
}
setDurability(durability: number) {
this.weapon.durability = durability;
}
setEnchantment(enchantment: string) {
this.weapon.enchantment = enchantment;
}
getResult() {
return this.weapon;
}
}
class WeaponDirector {
constructor(private builder: Builder) {}
createWeapon() {
this.builder.setName("Custom Weapon");
this.builder.setDamage(30);
this.builder.setDurability(60);
this.builder.setEnchantment("Ice");
return this.builder.getResult();
}
}
// Usage example
const basicSwordBuilder = new BasicSwordBuilder();
const fireSwordBuilder = new FireSwordBuilder();
const director = new WeaponDirector(basicSwordBuilder);
const basicSword = director.createWeapon();
director.builder = fireSwordBuilder;
const fireSword = director.createWeapon();
console.log(basicSword);
console.log(fireSword);

Here, we have two different WeaponBuilder classes (BasicSwordBuilder and FireSwordBuilder) that each create Weapon objects with different default properties. We also have a WeaponDirector class that takes a Builder object as a parameter and uses it to create custom Weapon objects with specific properties.

We then create instances of the BasicSwordBuilder and FireSwordBuilder classes, and use the WeaponDirector to create custom weapons with the BasicSwordBuilder and FireSwordBuilder.

🤓 How to use a Builder in TypeScript

Step 1: Define the Weapon class

class Weapon {
constructor(
public name: string,
public damage: number,
public durability: number,
public enchantment: string) {}
}

This class represents the objects that we want to create using the Builder pattern. It has four properties: name, damage, durability, and enchantment.

Step 2: Define the Builder interface

interface Builder {
setName(name: string): void;
setDamage(damage: number): void;
setDurability(durability: number): void;
setEnchantment(enchantment: string): void;
getResult(): Weapon;
}

This interface defines the methods that all Builder classes must implement in order to build Weapon objects. Each method corresponds to a Weapon property, and the getResult method returns the final Weapon object.

Step 3: Implement the BasicSwordBuilder class

class BasicSwordBuilder implements Builder {
private weapon: Weapon;
constructor() {
this.weapon = new Weapon("Basic Sword", 10, 50, "");
}
setName(name: string) {
this.weapon.name = name;
}
setDamage(damage: number) {
this.weapon.damage = damage;
}
setDurability(durability: number) {
this.weapon.durability = durability;
}
setEnchantment(enchantment: string) {
this.weapon.enchantment = enchantment;
}
getResult() {
return this.weapon;
}
}

This class implements the Builder interface to create Weapon objects with default properties for a basic sword.

Step 4: Implement the FireSwordBuilder class

class FireSwordBuilder implements Builder {
private weapon: Weapon;
constructor() {
this.weapon = new Weapon("Fire Sword", 20, 40, "Fire");
}
setName(name: string) {
this.weapon.name = name;
}
setDamage(damage: number) {
this.weapon.damage = damage;
}
setDurability(durability: number) {
this.weapon.durability = durability;
}
setEnchantment(enchantment: string) {
this.weapon.enchantment = enchantment;
}
getResult() {
return this.weapon;
}
}

This class implements the Builder interface to create Weapon objects with default properties for a fire sword.

Step 5: Implement the WeaponDirector class

class WeaponDirector {
constructor(private builder: Builder) {}

createWeapon() {
this.builder.setName("Custom Weapon");
this.builder.setDamage(30);
this.builder.setDurability(60);
this.builder.setEnchantment("Ice");
return this.builder.getResult();
}
}

This class takes a Builder object as a parameter, and uses it to create a custom Weapon object with specific properties.

Step 6: Use the Builder pattern to create custom weapons

const basicSwordBuilder = new BasicSwordBuilder();
const fireSwordBuilder = new FireSwordBuilder();
const director = new WeaponDirector(basicSwordBuilder);
// Create a basic sword using the director
const basicSword = director.createWeapon();
console.log(basicSword);
// Create a fire sword using the director
director.builder = fireSwordBuilder;
const fireSword = director.createWeapon();
console.log(fireSword);
// Create a custom sword without using the director
const customSword = new Weapon("Custom Sword", 40, 80, "Lightning");
console.log(customSword);

🥰 When to use ?

  1. To get rid of a massive constructor, use the Builder pattern.
  2. When you want your code to be able to generate various representations of a particular product, use the Builder pattern (for example, sword and magical 3000 sword of the light and right).

✅ Benefits

  1. It simplifies the construction of complex objects by breaking it down into smaller, simpler steps.
  2. It separates the construction of the object from its representation, making it easier to change the representation of the object without affecting its construction.
  3. It makes it easier to create different variations of the same object by using different builders.
  4. It makes the code more readable and maintainable by separating the construction logic from the rest of the code.

❌ Drawbacks

  1. The overall complexity of the code increases since the pattern requires creating multiple new classes.

😊 Conclusion

The builder pattern is a powerful design pattern that is used to create complex objects. By separating the construction of the object from its representation, it makes it easier to create complex objects step by step, and it also makes it easier to change the representation of the object without affecting its construction.

When using the builder pattern, it’s important to keep in mind that it’s not always necessary to use a builder for every object you create. The builder pattern is best used for objects that have a complex construction process or for objects that have several optional parameters. If an object has a simple construction process and only a few required parameters, it may not be necessary to use a builder.

In addition, it’s important to note that the builder pattern is not the only design pattern that can be used to create complex objects. Other design patterns, such as the factory pattern and the abstract factory pattern, can also be used to create complex objects. The choice of which design pattern to use depends on the specific requirements of the project.

Overall, the builder pattern is a useful tool to have in your toolbox. It simplifies the construction of complex objects, makes the code more readable and maintainable, and allows for different variations of the same object to be created easily. By understanding the benefits and limitations of the builder pattern, you can use it effectively in your projects.

You can find more explanations and code examples in my repository:

Hope you enjoyed this article! ❤️

--

--