In this post, we will learn how to use the builder pattern in Javascript, and see some code examples, as well as advanced concepts like validation and fixed attribute options.
If you just want to see the example code, you can view it on Github
Basic Usage - Creating a Builder Class
The builder pattern is a popular object oriented design pattern that makes it easier to construct object instances.
For example, consider a Car
class with some properties:
- The
color
of the car - Whether it has a
spoiler
or not - Its
fuelType
- this should either be “petrol” or “diesel” - Its
productionDate
, which should be a JavascriptDate
type
Let’s see how we can define it’s class:
class Car {
// these attributes will be private, so we prefix them with `#`
#color = null
#spoiler = null
#fuelType = null
#productionDate = null
constructor(color, spoiler, fuelType, productionDate) {
this.#color = color
this.#spoiler = spoiler
this.#fuelType = fuelType
this.#productionDate = productionDate
}
// we should be able to print information about the car according to its parameters
toString() {
return `color: ${this.#color}
spoiler: ${this.#spoiler}
fuel type: ${this.#fuelType}
production date: ${this.#productionDate}`
}
}
We can now construct an instance and print it to the console:
const car = new Car('red', true, 'petrol', new Date('2020-09-21'))
console.log(car.toString())
This should give the output:
color: red
spoiler: false
fuel type: petrol
production date: Tue Sep 21 2021 05:30:00 GMT+0530
Let’s look at some of the problems with this code that can cause bugs and readability issues:
- You need to remember the order of the arguments: Someone trying to construct a new
Car
may get confused which argument is forcolor
, and which is for thefuelType
. - Some attributes, like
fuelType
, are supposed to have a fixed number of possible values (in this case"petrol"
or"diesel"
) - it is easy to make an error here (what if we misspell"diesel"
as"deisel"
?)
These problems will only get worse for objects that have more attributes.
A builder class is a special class that helps us specify each attribute in a method, before finally constructing the intended object.
To make a builder for the above example, we can create a static Builder
class within the parent Car
class:
class Car {
#color = null
#spoiler = null
#fuelType = null
#productionDate = null
// We define a static Builder class within `Car`
static Builder = class {
// the builder class will have the same attributes as
// the parent
#color = null
#spoiler = null
#fuelType = null
#productionDate = null
// there are four methods to set each of the four
// attributes
setColor(color) {
this.#color = color
// each method returns the builder object itself
// this allows for chaining of methods
return this
}
setSpoiler(spoiler) {
this.#spoiler = spoiler
return this
}
setFuelType(fuelType) {
this.#fuelType = fuelType
return this
}
setProductionDate(date) {
this.#productionDate = date
return this
}
// when we're done setting arguments, we can call the build method
// to give us the `Car` instance
build() {
const car = new Car(
this.#color,
this.#spoiler,
this.#fuelType,
this.#productionDate)
return car
}
}
constructor(color, spoiler, fuelType, productionDate) {
this.#color = color
this.#spoiler = spoiler
this.#fuelType = fuelType
this.#productionDate = productionDate
}
toString() {
return `color: ${this.#color}
spoiler: ${this.#spoiler}
fuel type: ${this.#fuelType}
production date: ${this.#productionDate}`
}
}
We can use the Builder
class to construct a Car
instance, instead of using the new Car(...)
constructor:
const car = new Car.Builder()
.setColor('red')
.setFuelType('petrol')
.setProductionDate(new Date('2021-09-21'))
.setSpoiler(false)
.build()
console.log(car.toString())
It’s now much easier to recognize different attributes when they’re being set. We can also change the order of the builder methods being called and get the same result.
Now let’s look at some alternate ways to implement our builder methods to better suit our requirements.
Setting Constant Attribute Values
Some of the attributes, like fuelType
only have a fixed number of values. Instead of placing the responsibility on the user of our class, we can create special builder methods to set the correct fuel type:
static Builder = class {
//...
// these methods don't take any arguments, and set the
// required hard-coded values
withPetrolFuelType() {
this.#fuelType = 'petrol'
return this
}
withDieselFuelType() {
this.#fuelType = 'diesel'
return this
}
// we can also do the same with boolean types
// since they only have two possible values
withSpoiler() {
this.#spoiler = true
return this
}
withOutSpoiler() {
this.#spoiler = false
return this
}
//...
}
This makes our code less error prone, since the only way to set the fuelType
is by choosing from one of the constant options provided:
const car = new Car.Builder()
.setColor('red')
// using the constant attribute method
.withPetrolFuelType()
.setProductionDate(new Date('2021-09-21'))
// In this case, the method is semantically more readable
.withOutSpoiler()
.build()
Adding Attribute Validation
We can use builder methods to add validation for our attributes.
For our example, productionDate
should be a valid Date
object. Let’s validate that in the setProductionDate
method:
setProductionDate(date) {
// check if `date` is a `Date` object, and that it
// is a valid date (isNan should be false)
if (!(date instanceof Date) || isNaN(date)) {
// if our conditions fail, throw an error
throw new Error('invalid production date')
}
this.#productionDate = date
return this
}
As opposed to adding all our validations in the constructor, we can add it to each builder method instead. This helps us separate each validation according to its attribute, and allows us to fail fast incase a validation fails.
Multiple Builder Types
Sometimes, there can be multiple use-cases to build the same type of object. In this case, we can make different builders depending on our use case.
Consider a race car: it’s like a regular car, but the fuelType
is always "petrol"
and it always has a spoiler.
We can create a new builder class for building race cars, which makes use of our original builder:
class Car {
// ...
static RaceCarBuilder = class {
#color = null
#productionDate = null
setColor(color) {
this.#color = color
return this
}
setProductionDate(date) {
this.#productionDate = date
return this
}
// We can use our original builder and fix specific attributes
// like `fuelType` and `spoiler`
build() {
return new Car.Builder()
.setColor(this.#color)
.setProductionDate(this.#productionDate)
.withPetrolFuelType()
.withSpoiler()
.build()
}
}
// ...
}
We can now make it easier to construct these kind of cars:
const raceCar = new Car.RaceCarBuilder()
.setColor('green')
.setProductionDate(new Date('2021-09-21'))
.build()
console.log(raceCar.toString())
Note that this is not the same as class inheritance. In this example, there is no separate RaceCar
class. We are just defining a different builder to help us construct a Car
instance that has some additional constraints.
To see the example code for all the examples discussed here, you can view the repo on Github
When to Use the Builder Pattern
The builder pattern is useful when we want to make the construction of an object easier and more readable.
In classes with more than five attributes, it’s difficult to call the constructor without forgetting the order of arguments.
For objects with private attributes, or with attributes that can only be set at the time of construction, using a builder allows us to set attributes at different points in our code.
However, there are some obvious downsides to using builders as well:
- Bloated code: Adding a
Builder
class within ourCar
class required us to add a lot more code than if we just used the constructor. Additionally, it can seem repetitive since the builder class required similar attributes to the main class. - We need to construct two objects now: the
Builder
instance, and the actualCar
itself. This is not ideal if your application is sensitive to memory constraints.
In general, it makes sense to add a builder class if your original class is expected to be used multiple times in your application code. It’s also a better tradeoff if there are a lot of attributes that need to be configured to build an object instance.
Can you think of any other uses or pitfalls of using the builder pattern in your Javascript application? Let me know in the comments!