DEV Community

loading...
Cover image for How to implement and use Builder pattern in JS

How to implement and use Builder pattern in JS

Cristian Curteanu
JS, Ruby and Go(lang) enthusiast
・2 min read

There might be cases when an object contains too many details to be passed via a constructor, and that might be the case to use builder pattern, so that an object setup could be done gradually, thus taking the complex construction of an object into smaller pieces

Let's consider a Car type abstraction:

class Car {
    brand;
    model;
}
Enter fullscreen mode Exit fullscreen mode

At this point the encapsulation of these fields are not relevant, as it can be added; and also the set of properties is kept minimal for the ease of understanding, although the Builder pattern could make sense for a more complex type.

The builder pattern, as it's representation, should take values from external world, that will be injected into Car object, that will also be contained by the builder. When the object is considered to have everything set up, the build method should be called, which basically will return the built object.

The following is a possible implementation of the Car builder:

class CarBuilder {
    #car;
    constructor(car = null) {
        this.#car = car || new Car();
    }

    madeBy(brand) {
        this.#car.brand = brand;
        return this;
    }

    model(model) {
        this.#car.model = model;
        return this;
    }

    build() {
        return this.#car;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that in this implementation, the Car object could be also injected into builder, which makes the implementation of the builder less coupled with the Car object itself. And this is how it can be used:

let carBuilder = new CarBuilder(new Car());
let car = carBuilder.madeBy("Toyota").model("Prius").build();

console.log(car) // => Car { brand: 'Toyota', model: 'Prius' }
Enter fullscreen mode Exit fullscreen mode

This way, the model name and brand name was passed to a Car object, using madeBy and model method of a separate abstraction.

This implementation, can be replaced to a more functional approach:

class FunctionalCarBuilder {
    actions = [];

    constructor(car) {
        this.car = car
    }

    madeBy(brand) {
        this.actions.push(function(car) {
            car.brand = brand;
        })
        return this;
    }

    model(model) {
        this.actions.push(function(car) {
            car.model = model;
        })
        return this
    }

    build() {
        for (let i = 0; i < this.actions.length; i++) {
            const build = this.actions[i];
            build(this.car)
        }
        return this.car
    }
}
Enter fullscreen mode Exit fullscreen mode

which can be used as follows:

let carBuilder = new FunctionalCarBuilder(new Car());
let car = carBuilder.madeBy("Toyota").model("Prius").build();

console.log(car) // => Car { brand: 'Toyota', model: 'Prius' }
Enter fullscreen mode Exit fullscreen mode

So it does have the same interface, however here we have a set of function objects, that basically are modifiers of the build object. It might be useful for the cases when we need to decouple the logic of value definition from the builder, and don't have any assignment parameter. To go even further, a modifier function can be passed as parameter on specific builder methods, and this way to improve the decoupling.

Conclusion

The builder pattern can be extremely useful when we have to deal with definition of an object with a complex structure, thus the object definition is delegated to separate abstraction, and the control of definition process is even better. Due to it's nature, JavaScript provides several ways of builder definitions; although the interface is the same, the approach and the mechanism of object construction would be different.

Discussion (0)