DEV Community

Cover image for Learn Design Patterns: Understanding the Builder Pattern
Sami Maachi
Sami Maachi

Posted on

Learn Design Patterns: Understanding the Builder Pattern

When creating objects with many optional or interdependent parts, maintaining clarity and flexibility in the construction process can become a challenge. The Builder Pattern addresses this by allowing step-by-step object creation, ensuring flexibility and scalability in your code.

In this article, we’ll explore the Builder Pattern with a practical gaming scenario, explaining the problem it solves, and illustrating how it works through a real-world implementation using TypeScript.

Table of Contents

  1. Builder Pattern Definition
  2. Problems Solved with Builder Pattern
  3. Real-Life Projects That Use the Builder Pattern

Builder Pattern Definition

The Builder Pattern is a creational design pattern that decouples the construction of a complex object from its representation.

Rather than using a class constructor overloaded with numerous parameters, which can make the code difficult to read and maintain, the Builder Pattern provides a way to construct objects step by step. This approach simplifies managing optional or interdependent components, resulting in more readable and maintainable code.

Problems Solved with Builder Pattern

Imagine you are designing a game where players can create customizable characters. Each character can have the following attributes:

  • Helmet

  • Torso armor

  • Arm covers

  • Pants

  • Primary weapon

  • Secondary weapon

  • Melee weapon

Now, consider the challenges:

  1. Complex Object Construction
    Some characters require full equipment (e.g., sniper characters), while others may start with only basic armor. Managing these configurations with traditional constructors becomes overwhelming.

  2. Flexibility in Customization
    Players need to create characters step by step, with the freedom to skip or modify parts of the configuration.

  3. Readability and Maintainability
    Directly initializing these objects using constructors or long parameter lists is not only error-prone but also hard to maintain as new features are added.

Implementation

Key Components

Step 1: Character Class
Represents the complex object being built, containing attributes like armor and weapons.

interface ICharacter {
  helmet: string | null;
  torsoArmor: string | null;
  armsCover: string | null;
  pants: string | null;
  primaryWeapon: string | null;
  secondaryWeapon: string | null;
  meleeWeapon: string | null;
}

class Character implements ICharacter {
  helmet: string | null = null;
  torsoArmor: string | null = null;
  armsCover: string | null = null;
  pants: string | null = null;
  primaryWeapon: string | null = null;
  secondaryWeapon: string | null = null;
  meleeWeapon: string | null = null;

  constructor() {}
}
Enter fullscreen mode Exit fullscreen mode

Step 2:ICharacterBuilder Interface
Defines the methods required to build each part of the character.

interface ICharacterBuilder {
  addHelmet(helmet: string): this;
  addTorsoArmor(armor: string): this;
  addArmsCover(armsCover: string): this;
  addPants(pants: string): this;
  addPrimaryWeapon(weapon: string): this;
  addSecondaryWeapon(weapon: string): this;
  addMeleeWeapon(weapon: string): this;
  getCharacter(): Character;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: CharacterBuilder Class
Implements the builder interface and provides methods for constructing each part of the character.

class CharacterBuilder implements ICharacterBuilder {
  private character: Character;

  constructor() {
    this.reset();
  }

  reset() {
    this.character = new Character();
  }

  addHelmet(helmet: string): this {
    this.character.helmet = helmet;
    return this;
  }

  addTorsoArmor(armor: string): this {
    this.character.torsoArmor = armor;
    return this;
  }

  addArmsCover(armsCover: string): this {
    this.character.armsCover = armsCover;
    return this;
  }

  addPants(pants: string): this {
    this.character.pants = pants;
    return this;
  }

  addPrimaryWeapon(weapon: string): this {
    this.character.primaryWeapon = weapon;
    return this;
  }

  addSecondaryWeapon(weapon: string): this {
    this.character.secondaryWeapon = weapon;
    return this;
  }

  addMeleeWeapon(weapon: string): this {
    this.character.meleeWeapon = weapon;
    return this;
  }

  getCharacter(): Character {
    const result = this.character;
    this.reset();
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Director Class
Orchestrates the building process by defining configurations for different character types.

class Director {
  private characterBuilder: CharacterBuilder;

  setCharacterBuilder(characterBuilder: CharacterBuilder) {
    this.characterBuilder = characterBuilder;
  }

  buildStarterCharacter() {
    this.characterBuilder
      .addHelmet("starter")
      .addTorsoArmor("starter")
      .addArmsCover("starter")
      .addPants("starter");
  }

  buildSniperCharacter() {
    this.characterBuilder
      .addHelmet("ghillie head cover")
      .addTorsoArmor("ghillie torso cover")
      .addArmsCover("ghillie arms cover")
      .addPants("ghillie pants")
      .addPrimaryWeapon("Bolt sniper rifle")
      .addSecondaryWeapon("pistol")
      .addMeleeWeapon("knife");
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Client Code
Demonstrates how the Builder Pattern is used to create different character configurations.

const director = new Director();
const builder = new CharacterBuilder();
director.setCharacterBuilder(builder);
director.buildStarterCharacter();
console.log(builder.getCharacter());

director.buildSniperCharacter();
console.log(builder.getCharacter());

Enter fullscreen mode Exit fullscreen mode




Problems Solved with Builder Pattern

  1. Simplifies Complex Object Creation
    By delegating the construction logic to a builder class, the client code becomes cleaner and more manageable.

  2. Improves Flexibility
    Clients can build objects step by step, adding only the parts they need.

  3. Promotes Scalability
    Adding new attributes or configurations (e.g., a wizard character) doesn’t disrupt existing code.

  4. Supports Readability
    The clear, incremental construction process is easier to understand and maintain.

Real-Life Projects That Use the Builder Pattern

  1. Game Development:
    Building customizable characters, levels, or items incrementally.

  2. UI Builders:
    Constructing dynamic user interfaces where optional components are added based on user input.

  3. Document Generators:
    Creating structured documents like PDFs or reports with optional sections.

  4. Query Builders:
    Incrementally building SQL queries or API requests.

The Builder Pattern is a versatile tool for managing complex object creation, offering flexibility and scalability while keeping your code maintainable and readable. With the provided implementation, you can now start applying this pattern to your projects!

In the next article of our Learn Design Patterns series, we’ll explore the Prototype Pattern, which is used to create new objects by cloning an existing object, known as a prototype. Stay tuned, and keep learning!

Top comments (0)