The Builder Pattern in JavaScript

Christopher T.
November 20th, 2019

When you're developing apps in JavaScript you sometimes find it difficult to construct objects that are complex. Once it hits this certain point in your code, it becomes more important as it can get way more complex as your app gets larger.

The good news is that there's ways to break this complexity down into simple and smaller steps. And that's what we will be going over today.

This article will be going over a design pattern that will help you in these situations.

The Builder Design Pattern

The design pattern we will be going over today is commonly known as the Builder Design Pattern, which is a pattern used to help construct complex objects. It helps separate object construction from its representation which will help us reuse this to create different representations.

It lays out the following steps:

  1. The base class contains the business logic

    • It also receives the object that was created and proceeds to set the values
  2. Separate the code that is responsible for creating objects into builders--which ultimately are also just objects/classes.

    • All of these builders will be responsible for defining the steps to construct the complex objects.
  3. Can use an optional class which is called the Director

    • Directors are involved in defining methods ensuring that steps are executed in a specific order to build the commonly constructed objects.

What other problems does the Builder Pattern solve?

As previously mentioned, the builder pattern is generally needed most when we need a way to help simplify constructions of complex objects, so the best time to introduce this into your code is when you're hitting that point or when they are becoming large.

Let's take a look at the example code below and see why it can be difficult to manage in the future:

class Frog {
  constructor(name, weight, height, gender) {
    this.name = name
    this.weight = weight // in lbs
    this.height = height // in inches
    this.gender = gender
  }

  eat(target) {
    console.log(`Eating target: ${target.name}`)
  }
}

Here we have a Frog class. What are some of the issues you can come up with by looking at the example above?

One issue you may or may not have come up with is the parameters. I am specifically referring to this line:

constructor(name, weight, height, gender) {

Our Frog class definition seems to be easy to understand seeing as how little lines we've consumed. However, it's a whole different story when we attempt to instantiate Frog instances:

const bob = new Frog('Bob', 9, 2.2, 'male')

If you decided to go on a six month vacation trip to the carribeans, how well do you think you will remember what those two parameters in the middle are referring to? You'd be forced to go back and check the source code to be able to understand clearly what they really mean.

This is especially an issue when both of those parameters are the same type! Any developer could easily mix up the position of the weight or height parameters when instantiating a Frog, and in a real world scenario these situations are especially important in industries like the health industry because one data-mismatch can potentially cost companies big money!

So how does this get simplified? You guessed it, using the Builder pattern!

Here's what the code would look like, simplified by the pattern:

class FrogBuilder {
  constructor(name, gender) {
    this.name = name
    this.gender = gender
  }

  setWeight(weight) {
    this.weight = weight
    return this
  }

  setHeight(height) {
    this.height = height
    return this
  }

  build() {
    if (!('weight' in this)) {
      throw new Error('Weight is missing')
    }
    if (!('height' in this)) {
      throw new Error('Height is missing')
    }
    return new Frog(this.name, this.weight, this.height, this.gender)
  }
}

const leon = new FrogBuilder('Leon', 'male')
  .setWeight(14)
  .setHeight(3.7)
  .build()

We can now clearly see what is happening when creating Frog instances!

Let's go ahead and look at the constructor and see what we gained from the rework:

constructor(name, gender) {
    this.name = name
    this.gender = gender
  }

The FrogBuilder cuts down the number of parameters when instantiating down from 4 to 2 because it moves them to its implementation details. This not only makes it easier to understand, but also looks more natural to read when instantiating:

const sally = new FrogBuilder('Sally', 'female')

Previously we could easily forget where, when, or how we set the weight and height properties on a Frog:

const bob = new Frog('Bob', 9, 2.2, 'male')

With the new approach, the builder (FrogBuilder) simplies this issue by encouraging for more open opportunities to work our way around that:

const sally = new FrogBuilder('Sally', 'female')
  .setWeight(5)
  .setHeight(7.8)
  .build()

console.log(sally)

/*
  Result: 
    {
      gender: "female",
      height: 7.8,
      name: "Sally",
      weight: 5
    }
*/

And just like that, we end up with the same result only now it's more manageable and readable to us humans!

Lets go ahead and take another example.

Lets imagine having a complex object that requires a large, systematic step-by-step initialization of abundant fields and nested objects. This can get buried inside a huge constructor with many parameters--or even scattered throughout your code.

For example, lets go ahead and think about how to create a Car object. In order to build a simple car, you would need to construct four wheels, a steering wheel, brakes, and gas pedals. But what if you want a truck with additional goodies like a sunroof and an air conditioner?

The seemingly easiest solution to this task is to extend the base Car class and create a set of subclasses, each covering pieces of the parameters. But if you think about it, you will eventually face the issue with having to create new subclasses every time you come across new parameters, such as having tinted windows. And every time this happens your hierarchy will grow even more.

The other approach involves creating one gigantic constructor right in the base Car class that handles all of the possible variations of parameters that control the car object. While this eliminates any needs for subclasses, it still introduces a new problem:

When you have a giant constructor that tries to handle every parameter, in most cases this is a bad idea because most of the parameters won't actually be used. This makes the constructor block unnecessarily hard to comprehend and maintain.

For example, most cars don't even need sunroofs, so the parameters related to sunroof can become quite useless 99% of the time.

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!



© jsmanifest 2019