4 Dangerous Problems in JavaScript Easily Solved by The Builder Design Pattern

jsmanifest

jsmanifest

Posted on November 25, 2019

4 Dangerous Problems in JavaScript Easily Solved by The Builder Design Pattern

Find me on medium

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 "complex"ity can come in several forms. One could be that your code gets repetitive when you're trying to create different variations of certain objects. Another one could be that attempting to create those variations of objects can become quite long because you'd be having to do the logic in one giant block somewhere, like during the constructor block of a class.

This article will go over these problems and will show how the Builder Design Pattern in JavaScript will make those problems much less of an issue.

So what are the problems that the Builder pattern can easily solve?

Let's first look at an example without the builder pattern, and then an example with the builder pattern so that i'm not the only one with a visual code example in mind as we're going along:

In the following code examples, we're defining a Frog class. We'll pretend that in order for the Frog class to be fully capable of living and venturing out in the wild without a problem, they would require two eyes, all four legs, a scent, a tongue, and a heart. Now obviously in the real world there's a lot more involved and it sounds ridiculous to require a scent to be able to live, but we'll just keep it both simple and interesting rather than being fully factual about everything. We can get our facts 100% correct in another post at another time :)

Without the builder pattern

class Frog {
  constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height) {
    this.name = name
    this.gender = gender
    this.eyes = eyes
    this.legs = legs
    this.scent = scent
    this.tongue = tongue
    this.heart = heart
    if (weight) {
      this.weight = weight
    }
    if (height) {
      this.height = height
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With the builder pattern

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

  setEyes(eyes) {
    this.eyes = eyes
    return this
  }

  setLegs(legs) {
    this.legs = legs
    return this
  }

  setScent(scent) {
    this.scent = scent
    return this
  }

  setTongue(tongue) {
    this.tongue = tongue
    return this
  }

  setHeart(heart) {
    this.heart = heart
    return this
  }

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

  setHeight(height) {
    this.height = height
    return this
  }
}
Enter fullscreen mode Exit fullscreen mode

Now this seems a little overkill because the builder pattern example is larger in code. But if you dig deeper into all the cases that would occur during the development of a potential frog application, you will see that by looking at these two examples, the code example with the builder pattern applied will slowly rise in promoting simplicity, maintainability, and opening more opportunities to implement robust functionality.

Here are the 4 problems that the Builder Design Pattern can easily solve in JavaScript:

1. Code clutter and confusion

It's not uncommon that errors and accidents occur from carelessness of developing in large sizes of function blocks. In addition, when there are too many things going on in a single block, it is easy to get confused.

So what what kind of situation would you get into when there are "too many things going on" in function blocks, like the constructor?

Going back at our first code example implemented without the builder pattern, lets assume we have to add in some additional logic in order to accept the passed in arguments before applying them into an instance:

class Frog {
  constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height) {
    if (!Array.isArray(legs)) {
      throw new Error('Parameter "legs" is not an array')
    }
    // Ensure that the first character is always capitalized
    this.name = name.charAt(0).toUpperCase() + name.slice(1)
    this.gender = gender
    // We are allowing the caller to pass in an array where the first index is the left eye and the 2nd is the right
    //    This is for convenience to make it easier for them.
    //    Or they can just pass in the eyes using the correct format if they want to
    //    We must transform it into the object format if they chose the array approach
    //      because some internal API uses this format
    this.eyes = Array.isArray(eyes) ? { left: eye[0], right: eye[1] } : eyes
    this.legs = legs
    this.scent = scent
    // Pretending some internal API changed the field name of the frog's tongue from "tongueWidth" to "width"
    //    Check for old implementation and migrate them to the new field name
    const isOld = 'tongueWidth' in tongue
    if (isOld) {
      const newTongue = { ...tongue }
      delete newTongue['tongueWidth']
      newTongue.width = tongue.width
      this.tongue = newTongue
    } else {
      this.tongue = newTongue
    }
    this.heart = heart
    if (typeof weight !== 'undefined') {
      this.weight = weight
    }
    if (typeof height !== 'undefined') {
      this.height = height
    }
  }
}

const larry = new Frog(
  'larry',
  'male',
  [{ volume: 1.1 }, { volume: 1.12 }],
  [{ size: 'small' }, { size: 'small' }, { size: 'small' }, { size: 'small' }],
  'sweaty socks',
  { tongueWidth: 18, color: 'dark red', type: 'round' },
  { rate: 22 },
  6,
  3.5,
)
Enter fullscreen mode Exit fullscreen mode

Our constructor is a little long, and in some cases it doesn't even seem like a lot of the logic won't even be necessary. It is cluttered by logic of handling different parameters. This can be confusing especially if we haven't looked at the source code of this in a long time.

When we're developing a frog application and we want to instantiate an instance of a Frog, the downside is that we would have to make sure that we get every parameter near 100% perfect in terms of following the function signature or something will throw during the construction phase. If we need to double check the type of eyes at some point, we would have to scan through the clutter of code to get to the code we're looking for. Would you start being confused if you finally found the lines you were looking for, but then realized there was another line of code that was referencing and affecting the same parameter just 50 lines above? Now you have to go back and scan through those to be able to understand what will happen.

If we take another look at the FrogBuilder constructor from an earlier example, we're able to simplify the constructor to feel more "natural" while removing the confusion. We would still be doing the extra validations, it would just be isolated into their own little methods, which is the heart and soul of the builder pattern.

2. Readability

If we take a look at the most recent code example, it's already getting a little hard to read because we have to process these different variations of handling at once. There's no way around it than to understand the whole thing at once if we wanted to create instances of a Frog.

In addition, we have to provide some documentation otherwise we'd be unsure why in the world is tongueWidth being renamed to width. This is absurd!

If we convert the example to use the builder pattern, we can make things more easily readable:

class FrogBuilder {
  constructor(name, gender) {
    // Ensure that the first character is always capitalized
    this.name = name.charAt(0).toUpperCase() + name.slice(1)
    this.gender = gender
  }

  formatEyesCorrectly(eyes) {
    return Array.isArray(eyes) ? { left: eye[0], right: eye[1] } : eyes
  }

  setEyes(eyes) {
    this.eyes = this.formatEyes(eyes)
    return this
  }

  setLegs(legs) {
    if (!Array.isArray(legs)) {
      throw new Error('"legs" is not an array')
    }
    this.legs = legs
    return this
  }

  setScent(scent) {
    this.scent = scent
    return this
  }

  updateTongueWidthFieldName(tongue) {
    const newTongue = { ...tongue }
    delete newTongue['tongueWidth']
    newTongue.width = tongue.width
    return newTongue
  }

  setTongue(tongue) {
    const isOld = 'tongueWidth' in tongue
    this.tongue = isOld
      ? this.updateTongueWidthFieldName(tongue, tongue.tongueWidth)
      : tongue
    return this
  }

  setHeart(heart) {
    this.heart = heart
    return this
  }

  setWeight(weight) {
    if (typeof weight !== 'undefined') {
      this.weight = weight
    }
    return this
  }

  setHeight(height) {
    if (typeof height !== 'undefined') {
      this.height = height
    }
    return this
  }

  build() {
    return new Frog(
      this.name,
      this.gender,
      this.eyes,
      this.legs,
      this.scent,
      this.tongue,
      this.heart,
      this.weight,
      this.height,
    )
  }
}

const larry = new FrogBuilder('larry', 'male')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('sweaty socks')
  .setHeart({ rate: 22 })
  .setWeight(6)
  .setHeight(3.5)
  .setLegs([
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
  ])
  .setTongue({ tongueWidth: 18, color: 'dark red', type: 'round' })
  .build()
Enter fullscreen mode Exit fullscreen mode

We gained the ability to make our code much more readable in a couple of ways:

  1. The names of the methods are sufficiently self-documenting
  • updateTongueWidthFieldName easily defines to us what it does and why it's doing it. We know that its updating the field name. And we also know why because the word "update" already means to bring up to date! This self-documented code helps us assume that some field name is old and needs to be changed to use the new field name.
  1. The constructor is short and simplified.
  • It's perfectly fine to set the other properties later!
  1. Can clearly understand each parameter when initiating a new Frog
  • It's like reading English. You're clearly setting the eyes, legs, etc and finally invoking the build method to create a Frog.
  1. Each logic is now isolated in separate blocks where we can easily follow through
  • When you're doing some changes you only need to focus on one thing, which is what ever that got isolated in function blocks.

3. Lack of control

The most important one on this list is benefiting from more control over the implementation. Prior to the builder example, it is possible to write more code in the constructor, but the more code you try to stick in there the more it degrades readability which causes clutter and confusion.

Since we're able to isolate implementation details to each of their own function blocks, we now have finer control in many ways.

One way is that we can add validations without even adding more problems, which makes the construction phase more robust:

setHeart(heart) {
  if (typeof heart !== 'object') {
    throw new Error('heart is not an object')
  }
  if (!('rate' in heart)) {
    throw new Error('rate in heart is undefined')
  }
  // Assume the caller wants to pass in a callback to receive the current frog's weight and height that he or she has set
  //    previously so they can calculate the heart object on the fly. Useful for loops of collections
  if (typeof heart === 'function') {
    this.heart = heart({
      weight: this.weight,
      height: this.height
    })
  } else {
    this.heart = heart
  }

  return this
}

validate() {
  const requiredFields = ['name', 'gender', 'eyes', 'legs', 'scent', 'tongue', 'heart']
  for (let index = 0; index < requiredFields.length; index++) {
    const field = requiredFields[index]
    // Immediately return false since we are missing a parameter
    if (!(field in this)) {
      return false
    }
  }
  return true
}

build() {
  const isValid = this.validate(this)
  if (isValid) {
  return new Frog(
    this.name,
    this.gender,
    this.eyes,
    this.legs,
    this.scent,
    this.tongue,
    this.heart,
    this.weight,
    this.height,
  )
  } else {
    // just going to log to console
    console.error('Parameters are invalid')
  }
}
Enter fullscreen mode Exit fullscreen mode

We took advantage of the fact that each part of the constructor is isolated by adding in validations as well as a validate method to ensure that all of the required fields have been set before finally building the Frog.

We can also take advantage of these opened opportunities to add further custom input data types to build the original return value of a parameter.

For example we can add more custom ways the caller can pass in eyes, to provide them even more convenience than what we previously provided:

formatEyesCorrectly(eyes) {
  // Assume the caller wants to pass in an array where the first index is the left
  //    eye, and the 2nd is the right
  if (Array.isArray(eyes)) {
    return {
      left: eye[0],
      right: eye[1]
    }
  }
  // Assume that the caller wants to use a number to indicate that both eyes have the exact same volume
  if (typeof eyes === 'number') {
    return {
      left: { volume: eyes },
      right: { volume: eyes },
    }
  }
  // Assume that the caller might be unsure of what to set the eyes at this current moment, so he expects
  //    the current instance as arguments to their callback handler so they can calculate the eyes by themselves
  if (typeof eyes === 'function') {
    return eyes(this)
  }

    // Assume the caller is passing in the directly formatted object if the code gets here
  return eyes
}

setEyes(eyes) {
  this.eyes = this.formatEyes(eyes)
  return this
}
Enter fullscreen mode Exit fullscreen mode

This way it makes it easier for the caller to choose any variation of input types they want:

// variation 1 (left eye = index 1, right eye = index 2)
larry.setEyes([{ volume: 1 }, { volume: 1.2 }])

// variation 2 (left eye + right eye = same values)
larry.setEyes(1.1)

// variation 3 (the caller calls the shots on calculating the left and right eyes)
larry.setEyes(function(instance) {
  let leftEye, rightEye
  let weight, height
  if ('weight' in instance) {
    weight = instance.weight
  }
  if ('height' in instance) {
    height = instance.height
  }

  if (weight > 10) {
    // It's a fat frog. Their eyes are probably humongous!
    leftEye = { volume: 5 }
    rightEye = { volume: 5 }
  } else {
    const volume = someApi.getVolume(weight, height)
    leftEye = { volume }
    // Assuming that female frogs have shorter right eyes for some odd reason
    rightEye = { volume: instance.gender === 'female' ? 0.8 : 1 }
  }

  return {
    left: leftEye,
    right: rightEye,
  }
})

// variation 4 (caller decides to use the formatted object directly)
larry.setEyes({
  left: { volume: 1.5 },
  right: { volume: 1.51 },
})
Enter fullscreen mode Exit fullscreen mode

4. Boilerplate (Solved by: Templating)

One concern we might come across in the future is that we end up with some repetitive code.

For example, looking back at our Frog class, do you think that when we want to create certain types of frogs, some of them might have the same exact traits?

In a real world scenario, there are different variations of frogs. A toad for example is a type of a frog, but not all frogs are toads. So that tells us that there are some distinctive properties of a toad that should not belong to normal frogs.

One difference between toads and frogs is that toads spend most of their time on land as opposed to normal frogs who spend most of their time inside water. In addition, toads also have dry bumpy skin whereas the skin of normal frogs are a little slimy.

That means we're going to have to ensure some how that every time a frog is instantiated, only some values can make it through as well as some values must make it through.

Let's go back to our Frog constructor and add in two new parameters: habitat, and skin:

class Frog {
  constructor(
    name,
    gender,
    eyes,
    legs,
    scent,
    tongue,
    heart,
    habitat,
    skin,
    weight,
    height,
  ) {
    this.name = name
    this.gender = gender
    this.eyes = eyes
    this.legs = legs
    this.scent = scent
    this.tongue = tongue
    this.heart = heart
    this.habitat = habitat
    this.skin = skin
    if (weight) {
      this.weight = weight
    }
    if (height) {
      this.height = height
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Making two simple changes to this constructor was already a little confusing! This is why the builder pattern is recommended. If we put the habitat and skin parameters at the end, it might cause bugs because weight and height can possibly be undefined since they are both optional! And since they are optional, if the caller doesn't pass those in, then habitat and skin will mistakenly be used for them. Yikes!

Lets edit the FrogBuilder to support habitat and skin:

setHabitat(habitat) {
  this.habitat = habitat
}

setSkin(skin) {
  this.skin = skin
}

Enter fullscreen mode Exit fullscreen mode

Lets now pretend we need to create 2 separate toads and 1 normal frog:

// frog
const sally = new FrogBuilder('sally', 'female')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('blueberry')
  .setHeart({ rate: 12 })
  .setWeight(5)
  .setHeight(3.1)
  .setLegs([
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
  ])
  .setTongue({ width: 12, color: 'navy blue', type: 'round' })
  .setHabitat('water')
  .setSkin('oily')
  .build()

// toad
const kelly = new FrogBuilder('kelly', 'female')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('black ice')
  .setHeart({ rate: 11 })
  .setWeight(5)
  .setHeight(3.1)
  .setLegs([
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
    { size: 'small' },
  ])
  .setTongue({ width: 12.5, color: 'olive', type: 'round' })
  .setHabitat('land')
  .setSkin('dry')
  .build()

// toad
const mike = new FrogBuilder('mike', 'male')
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('smelly socks')
  .setHeart({ rate: 15 })
  .setWeight(12)
  .setHeight(5.2)
  .setLegs([
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
  ])
  .setTongue({ width: 12.5, color: 'olive', type: 'round' })
  .setHabitat('land')
  .setSkin('dry')
  .build()
Enter fullscreen mode Exit fullscreen mode

So where is the repetitive code in this?

If we look closely, notice we have to repeat the toad's habitat and skin setters. What if there were 5 more setters that are exclusive only to toads? We would have to manually apply this template for toads every time we create them--the same goes for normal frogs.

What we can do is to create a templater, which is normally by convention called the Director.

The Director is responsible for executing steps to create objects--usually where there are some common structures that could be defined beforehand when building the final object, like in this case our toad.

So instead of having to manually set the distinctive properties between toads, we can have the director generate that template for us:

class ToadBuilder {
  constructor(frogBuilder) {
    this.builder = frogBuilder
  }
  createToad() {
    return this.builder.setHabitat('land').setSkin('dry')
  }
}

let mike = new FrogBuilder('mike', 'male')
mike = new ToadBuilder(mike)
  .setEyes([{ volume: 1.1 }, { volume: 1.12 }])
  .setScent('smelly socks')
  .setHeart({ rate: 15 })
  .setWeight(12)
  .setHeight(5.2)
  .setLegs([
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
    { size: 'medium' },
  ])
  .setTongue({ width: 12.5, color: 'olive', type: 'round' })
  .build()
Enter fullscreen mode Exit fullscreen mode

That way, you avoid implementing the boilerplate that all toads share in common and can focus only on the properties you need. This becomes more useful when there are even more properties exclusive only to toads.

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!

Find me on medium

💖 💪 🙅 🚩
jsmanifest
jsmanifest

Posted on November 25, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related