π Builder Pattern with a Fluent API in JavaScript
- Published on
Imagine a scenario where you want to create a Task object. A Task usually has a title, a due date, maybe a description, in some cases an assignee.
The simplest solution is to create a base Task
and extend it to cover all possible combinations of options or parameters.
class Task {
constructor(title) {}
constructor(title, dueOn) {}
constructor(title, dueOn, assignee) {}
consturctor(title, dueOn, assignee, description) {}
constructor(title, dueOn, assignee, description, status) {}
}
// Object with no Due data
new Task('Write Snippet', null, 'Dhanraj')
// Object with all the values
new Task(
'Write Snippet',
'2021-06-02',
'Dhanraj',
'Write a Snippet about Builder Patterns in JavaScript'
)
This is called the Telescoping Constructor Pattern. The problem with this pattern is that once constructors are 4 or 5 parameters long it becomes difficult to remember the required order of the parameters as well as what particular constructor you might want in a given situation.
In most cases, not all parameters will be used, making the constructor calls pretty ugly with null
or ''
. Come back to the code after a 3 weeks vacation, and you have no clue what the null
parameter does unless you look at the specific constructor.
The Solution: Builder Pattern
new Task()
.title('Builder Pattern')
.dueOn('2021-06-02')
.assignedTo('Me')
.description('Write a Snippet about implementing Builder Patterns in JavaScript');
The above code follows what is called a Builder Pattern.
The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns. --- Effective Java, 2nd Edition by Joshua Bloch
Now let's re-write the Task class using the Builder Pattern. First let's create a Task
class, which is the object we intend to use. It will encapsulate all the parameters and business logic to do what it needs to do.
class Task {
constructor(builder) {
this.title = builder._title
this.dueOn = builder._dueOn
this.assignee = builder._assignee
this.description = builder._description
this.status = builder._status
}
/* Some Business logic and abstract/generic methods here */
}
Next step is to create the builder which will create the instance of Task
and provide a fluent API to the user
class TaskBuilder {
title = (_title) => {
this._title = _title
return this
}
dueOn = (_dueOn) => {
this._dueOn = _dueOn
return this
}
assignee = (_assignee) => {
this._assignee = _assignee
return this
}
description = (_description) => {
this._description = _description
return this
}
status = (_status) => {
this._status = _status
}
save = () => new Task(this)
}
The TaskBuilder
returns functions which set the attributes for the Task
instance. Notice how each function is in charge of setting the object's properties, this means that we can add validations inside them when required.
If you don't fancy arrow functions =>
, expand to see a simplified version
=>
, expand to see a simplified versionclass TaskBuilder {
title(_title) {
this._title = _title
return this
}
dueOn(_dueOn) {
this._dueOn = _dueOn
return this
}
assignee(_assignee) {
this._assignee = _assignee
return this
}
description(_description) {
this._description = _description
return this
}
status(_status) {
this._status = _status
}
save() {
return new Task(this)
}
}
Note that each function returns this
, the reference to the current object. This enables chaining of the function calls. The save()
method is invoked when all the properties are set. If needed, the save method can be invoked at a later point in time, deferring the object construction.
const task = new TaskBuilder()
.title('Builder Pattern')
.dueOn('2021-06-02')
.assignee('Me')
.description('Write a Snippet about Builder Patterns in JavaScript')
.save()
console.log(task)
// {title: 'Builder Pattern', dueOn: '2021-06-02', assignee: 'Me', description: 'Write a Snippet about Builder Patterns in JavaScript'}
This results in code that is easy to write and very easy to read and understand. This pattern is flexible, and it is easy to add more parameters to it in the future. It is really only useful if you are going to have more than 4 or 5 parameters for a constructor.
Pros and Cons
β Pros | β Cons |
---|---|
You can construct objects step-by-step, defer construction steps or run steps recursively. | The overall complexity of the code increases since the pattern requires creating multiple new classes. |
You can reuse the same construction code when building various representations of products. | It is really only useful if you are going to have more than 4 or 5 parameters for a constructor. |
Single Responsibility Principle. You can isolate complex construction code from the business logic of the product. |
Thatβs all for the builder pattern.