DEV Community

Cover image for Manage telescopic constructors with builder pattern using Typescript
Ibrahim Shamma
Ibrahim Shamma

Posted on • Updated on

Manage telescopic constructors with builder pattern using Typescript

This is a beginning of a series that targets code design in Typescript, your insights are invaluable.
The following blog is influenced by refactoring guru

The Problem

You have a schema bloated with optional attributes, how do mange constructing it?

// Schema
class House {
  public windows?: number;
  public doors?: number;
  public rooms?: number;
  public garage?: boolean;
  public walls?: boolean;
  public swimmingPool?: boolean;
  public garden?: boolean;
  public fence?: boolean;

  constructor(data: {
    windows?: number;
    doors?: number;
    rooms?: number;
    garage?: boolean;
    walls?: boolean;
    swimmingPool?: boolean;
    garden?: boolean;
    fence?: boolean;
  }) {
    this.windows = data.windows;
    this.doors = data.doors;
    this.rooms = data.rooms;
    this.garage = data.garage;
    this.walls = data.walls;
    this.swimmingPool = data.swimmingPool;
    this.garden = data.garden;
    this.fence = data.fence;
  }

  addWindows = (windows: number) => {
    this.windows = this.windows ? windows + this.windows : windows;
  };}

// Constructing Schema
const house = new House({
  doors: 4,
  windows: 4,
  rooms: 4,
  garage: true,
  walls: true,
  garden: true,
  fence: true,
});
Enter fullscreen mode Exit fullscreen mode

As you can see from the above example, each time you create instance of the class you will need to specify every needed attribute, which can be tedious, also error prone.

The Solution

Builder Pattern

This pattern achieves the ability to construct objections by using steps, the pattern allows you to produce different types and representations of an object using the same construction code.

Steps:

1. Create an interface for our class

abstract class House {
  public abstract windows: number;
  public abstract doors: number;
  public abstract rooms: number;
  public abstract garage: boolean;
  public abstract walls: boolean;
  public abstract swimmingPool: boolean;
  public abstract garden: boolean;
  public abstract fence: boolean;
}

class HouseA extends House {
  public windows!: number;
  public doors!: number;
  public rooms!: number;
  public garage!: boolean;
  public walls!: boolean;
  public swimmingPool!: boolean;
  public garden!: boolean;
  public fence!: boolean;

  addWindows = (windows: number) => (this.windows += windows);
}

class HouseB implements House {
  constructor(
    public windows: number,
    public doors: number,
    public rooms: number,
    public garage: boolean,
    public walls: boolean,
    public swimmingPool: boolean,
    public garden: boolean,
    public fence: boolean
  ) {}

  addWindows = (windows: number) => (this.windows += this.rooms * windows);
}
Enter fullscreen mode Exit fullscreen mode

Remember Dependency inversion principal that High-level classes shouldn’t depend on low-level classes. Both should depend on abstractions. Abstractions shouldn’t depend on details. Details should depend on abstractions.

2. Create Builder interface and concrete implementation

abstract class HouseBuilder {
  protected house!: House;
  public abstract buildWalls: () => void;
  public abstract buildDoors: () => void;
  public abstract buildWindows: () => void;
  public abstract buildGarage: () => void;
  public abstract buildRooms: () => void;
  public abstract buildSwimmingPool: () => void;
  public abstract buildGarden: () => void;
  public abstract buildFence: () => void;
  public abstract reset: () => void;
  public abstract getResults: () => House;
}

class ConcreteHouseBuilderA extends HouseBuilder {
  house!: HouseA;

  reset = () => {
    this.house = new HouseA();
  };

  buildDoors = () => {
    this.house.doors = 2;
  };

  buildWindows = () => {
    this.house.windows = 2;
  };
  // You can continue the rest of the building methods
}
Enter fullscreen mode Exit fullscreen mode
What did we do here?
  • Created interface for HouseBuilder in case we need to create different concrete implementations for each House concrete class
  • We initialized inside the concrete the instance of HouseA meanwhile the interface was reliant on House this will help us in creating more concrete builders
  • The main intent of builder class is to construct HouseA via steps

3. Create Director class

This is optional method, which wraps the builder implementation, this can help if we need to run builder steps in specific order, building walls before windows, etc.

Code:

type HouseType = "simple" | "luxury" | "modern";

class Director {
  constructor(private builder: HouseBuilder) {}
  changeBuilder = (builder: HouseBuilder) => (this.builder = builder);
  reset = () => {
    this.builder.reset();
  };

  buildBasicHouse = () => {
    this.builder.buildDoors();
    this.builder.buildWindows();
    this.builder.buildRooms();
  };

  make = (type: HouseType) => {
    this.builder.reset();
    switch (type) {
      case "simple":
        this.builder.buildGarage();
        break;
      case "luxury":
        this.builder.buildGarage();
        this.builder.buildGarden();
        this.builder.buildFence();
        break;
      case "modern":
        this.builder.buildGarage();
        this.builder.buildGarden();
        this.builder.buildSwimmingPool();
        this.builder.buildFence();
        break;
      default:
        break;
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

Note that Director is reliant on Builder interface

Now implementation time:

const concreteHouseA = new ConcreteHouseBuilderA();
const director = new Director(concreteHouseA);
director.make("modern");
console.log(concrete1.getResults());
Enter fullscreen mode Exit fullscreen mode

Conclusion

To sum up, using the Builder Pattern provides a simplified way to deal with the complexity of creating objects that have telescopic constructors. This design makes the construction process easier to interpret, more flexible, and easier to maintain by breaking it up into discrete parts. Code that is modular and readily extensible is encouraged by the use of interfaces and concrete implementations, which follow the Dependency Inversion Principle. Moreover, the optional Director class adds another level of structure by allowing building stages to be executed one after the other and making it easier to create various object configurations. Adopting such design principles increases overall project scalability, developer productivity, and code quality. We hope to learn more about code design techniques in this course, utilizing TypeScript's features to create beautiful and effective solutions. Watch this space for more insights and practical examples to elevate your coding proficiency.

Top comments (0)