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
}
}
}
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
}
}
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,
)
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()
We gained the ability to make our code much more readable in a couple of ways:
- 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.
- The constructor is short and simplified.
- It's perfectly fine to set the other properties later!
- 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
.
- 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')
}
}
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
}
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 },
})
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
}
}
}
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
}
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()
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()
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
Top comments (10)
Thanks for a great post!
It's a great discussion of why the builder pattern is worthwhile, separating construction details from objects' behaviours once it's been set up.
Adding my 2c worth of JS style advice though (nothing specific to builder patterns btw): let's move away from methods like
constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height)
outright.In JS if you have a bag of arguments they're better placed in an argument object. Use multiple arguments when there couldn't be any confusion about which param is which when looking at some code that calls the function (i.e. when there's a natural ordering or when order doesn't matter).
So
For the Frog thing I'd use something like:
(the
=undefined
is not required but is a note to callers that these are optional)This adds a whole two characters and no complexity to the code but comes with big wins:
Builder pattern should be rebranded to “Java Stockholm Syndrome Pattern”.
This is the exactly the pattern that i hated the most in PHP and Java, every property of your object you need a get and a set because u can't mutate then directly from the instance of the class.
As someone who started out in assembler over four decades ago, this approach really bothers me just due to the overhead of all the blasted "functions for nothing"
I mean in ASM a HLL function call works out to a push to the stack of the current execution point, push of all the argument values, changing execution point, extending the stack for locals, then on exit de-allocating locals, de-allocating the parameters, and then popping the return address.
I don't even want to THINK about the nightmare an interpreted loosely typed language like JavaScript makes of that... but there's a reason you're better off in production code using a for loop instead of Array.each, as the old way runs circles around it, more so if you get the derpy needlessly and painfully cryptic arrow functions involved.
All these new JS functions with their callbacks, and techniques like this of applying even more methods to the class, just seem to create more code and more execution overhead for little if any real-world benefit. More so when you get into that "return this" so you can daisy-chain them TRASH. It was derpy when the monument to developer ignorance and incompetence that is jQuery did it, and I see little here to change my opinion on that topic. You're just producing more copies of the pointer slowing the code down dramatically and wasting more RAM.
Your first example is card stacked too with bad choices... I mean to check if it's a value or undefined, check for undefined. If there's that many parameters just omit them and use arguments[].
Whilst there could be advantages in clarity on the calling side of things, as well as more versatility in being able to omit values, I'm just not sure the performance drop and added complexity is worth it.
Hence why I like passing JSON-type objects and then using Object.assign to apply it to "this", or using a for-loop if specific validations are needed for some values. Simply add a object as a property of the class that contains your validation methods, then if it exists while looping run it.
As this:
Would -- to me at least -- be a far more useful format to deal with, particularly given how often JSON is used for information exchange or storage.
Just Object.assign in the constructor, or for loop to apply validation or applications as needed off a object property containing the handler methods, and be done with it.
As someone with a C background I can relate to this reasoning.
However in the end of the day, seeing that most resource heavy operations are DOM manipulations and network I/O, the costs for creating an extra builder class seem neglible compares to them.
Besides, big objects like that are created relatively rarely, so the overhead likely won't affect anything too much. And it's always possible to optimise if measurements show that this particular object takes too long to build.
Given the benefits of the builder pattern I can live with all of that. However I agree it's wise to use plain objects where possible, JS is tooled perfectly around them and in many cases its tools are just enough.
I wouldn't say the Builder pattern exists solely for legibility purposes. Being able to do things async / transactionally instead of a serial burst is one of the first use cases that comes to mind. Building something with a multi-stage setup is something I do a lot in C# for automating serial port hardware, such as detecting a USB HID is on the tree before attempting to open a port is much easier with a method like this.
It's much easier to do incremental construction rather than a try/catch around a giant constructor and requiring catches for 10 different failure modes in the client. In my experience, a non-builder approach makes the coupling between the caller and constructor very tight in order to accommodate the error handling in these situations.
I first learned ASM also, but I've come to terms that ASM exists in a world apart from higher level languages. I like to use patterns like this to ensure that, from a dev perspective, I can building something with a solid foundation and rely on the compiler to optimize for the machine code. And since this post is on JS and I mentioned compiling, I'll just mention that's why I now use Svelte 3. 😊
I totally felt the same reading this. What a waste of resources for literally no benefit apart from legibility, which is hardly a reason unless you don't care about performance.
This.
And if you wish stricter organization on a bigger project, just use damn typescript (it will also change your views of OOP, type systems and make you notice how stupid the builder pattern is).
One difference between those two approaches is:
In normal approach you had full object ready to go, which isn't the same with builder for this example.
Imagine if we always need to create a frog with 10 params (Base Frog for example), is it really better to do it builder way? I think it is not.
To be clear builder is great pattern, but we should always operate on object which is ready to go without extras ;)
I could see this being used in a big is project. What are the drawbacks of using an object as your Frog constructor parameter instead of changing the signature each time you want to add a new property?