An Idiosyncratic Blog

πŸ— Builder Pattern with a Fluent API in JavaScript

Published on
β€’ 6 minutes read

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

Builder Pattern - Wikipedia

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

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() {
    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.