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;
}
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;
}
}
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' }
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
}
}
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' }
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.
Top comments (0)