Skip to main content

Builder

The Builder pattern is a creational design pattern used in software engineering. It is especially useful when constructing a complex object with many optional and required fields. The Builder pattern helps to create an object step-by-step, separating the construction of a complex object from its representation. This makes the code more readable and maintainable, and it also allows for more control over the construction process.

Here's a high-level overview of the key components in the Builder pattern:

Builder pattern

Builder pattern in the Wikpedia

  1. Product: The complex object that needs to be created.
  2. Builder Interface: This interface declares the methods for creating the different parts of the Product object.
  3. Concrete Builder: This class implements the Builder interface and provides an implementation for the steps needed to construct the product. It maintains the product through the building process and offers a method to retrieve the final product.
  4. Director: This class controls the construction process. It accepts a Builder object and executes the necessary steps to produce a product. The Director is responsible for the sequence of construction steps.
  5. Client: The client creates the Director and Builder objects and then instructs the Director to construct the desired object using the Builder.

In the context of TypeScript the Builder pattern can be implemented by defining a 'Builder' class that incrementally builds up an object of another 'Product' class.

// Product
class Car {
public seats: number;
public engine: string;
public tripComputer: boolean;
// Other parts and functionalities
}

// Builder Interface
interface CarBuilder {
setSeats(seats: number): void;
setEngine(engine: string): void;
setTripComputer(hasTripComputer: boolean): void;
getResult(): Car;
}

// Concrete Builder
class ConcreteCarBuilder implements CarBuilder {
private car: Car;

constructor() {
this.car = new Car();
}

setSeats(seats: number): void {
this.car.seats = seats;
}

setEngine(engine: string): void {
this.car.engine = engine;
}

setTripComputer(hasTripComputer: boolean): void {
this.car.tripComputer = hasTripComputer;
}

getResult(): Car {
return this.car;
}
}

// Director
class CarDirector {
constructSportsCar(builder: CarBuilder): Car {
builder.setSeats(2);
builder.setEngine("V8");
builder.setTripComputer(true);
return builder.getResult();
}
}

// Client
const builder = new ConcreteCarBuilder();
const director = new CarDirector();
const sportsCar = director.constructSportsCar(builder);

In this example, Car is the product. CarBuilder is the builder interface, and ConcreteCarBuilder is the concrete implementation of this interface. CarDirector is the director class that orchestrates the building process.

Functional Programming implementation

This approach will ensure immutability throughout the entire structure, aligning it more closely with functional programming principles. Each function will create a new state of the car and a new module instance, ensuring no mutable shared state.

const CarBuilder = (car = {}) => {
type Car = {
seats?: number;
engine?: string;
tripComputer?: boolean;
};

const setSeats = (seats: number) => {
return CarBuilder({ ...car, seats });
};

const setEngine = (engine: string) => {
return CarBuilder({ ...car, engine });
};

const setTripComputer = (tripComputer: boolean) => {
return CarBuilder({ ...car, tripComputer });
};

const build = (): Car => {
return car as Car;
};

return {
setSeats,
setEngine,
setTripComputer,
build
};
};

// Usage
const car = CarBuilder().setSeats(4).setEngine("V6").setTripComputer(true).build();
console.log(car);

In this version:

  • CarBuilder is now a function that takes an optional car parameter and returns a new module.
  • setSeats, setEngine, and setTripComputer modify the car state and return a new CarBuilder invocation with the updated state.
  • Each call to these functions results in a completely new instance of the module, maintaining immutability.

Considerations

  • When designing with immutability in mind, consider the impact on performance and memory usage, as each change creates a new instance.
  • This approach can be particularly useful in React applications, where immutability helps prevent unintended side-effects and makes state changes more predictable.
  • Always test and validate the scalability of such patterns in your application, especially if you are dealing with complex states or a large number of state updates.

Tips for Fullstack Developers

  • When applying design patterns like the Builder, it's important to evaluate whether the complexity it adds is justified by the complexity of the object being built.
  • In frontend development, particularly with frameworks like React, design patterns can help structure your components and state management effectively, but always aim for simplicity and readability.