xpcoffee icon
This is my site. Please treat it gently. ❤

Builder

TLDR A "builder" is a pattern which provides a more user-friendly interface for configuring and creating an object.

Problem/context

Classes with a lot of functionality can grow to have many optional parameters and many allowable configurations. These interfaces can become confusing and cumbersome to use. They can also be problematic to use if you don't have all the necessary parameters up-front, forcing the user to progressively keep a reference to parameters until all of them have been gathered.

Concept

We create a class dedicated to tracking configuration choices and that exposes an API which allows users to specify options one at a time. When the user is ready, they can then ask this new class to create a new instance of the object.

Key characteristics

  • Specifying configuration and creating a new instance of an object are separate actions.
  • Configuration methods return the builder object to enable call-chaining.
  • The builder objects are stateful: they store configuration options.

Examples

A builder class that creates a new car object with custom configuration.

// Note: several class/enum definitions have been left out for brevity

const snazzyCar = new CarBuilder()
  .withDriveType(new ManualDrive())
  .withSeatMaterial(Material.SUEDE)
  .withRoofType(new Convertible())
  .build()

const wellPricedCar = new CarBuilder().withCheapestOptions().build()

class CarBuilder {
  // Builders can also be used to define defaults to reduce the need for users to configure them.
  // Different builders can specify different sets of defaults.
  private seatMaterial: Material = Material.LEATHER
  private driveType: DriveType = new AutomaticDrive()
  private roofType: RoofType = new Hardtop()

  withSeatMaterial(seatMaterial: Material) {
    this.seatMaterial = seatMaterial
    return this // returning this object to allow method chaining
  }

  withDriveType(driveType: DriveType) {
    this.driveType = driveType
    return this
  }

  withRoofType(roofType: RoofType) {
    this.roofType = roofType
    return this
  }

  // configuration methods can also configure groups of options
  withCheapestOptions() {
    this.seatMaterial = Material.PLASTIC
    this.driveType = new ManualDrive()
    this.roofType = new Hardtop()
  }

  build() {
    return new Car({
      seatMaterial: this.seatMaterial,
      driveType: this.driveType,
      roofType: this.roofType,
    })
  }
}