DEV Community

Thomas Noe
Thomas Noe

Posted on • Originally published at t3h2mas.xyz on

Fluent Builder with JavaScript

construction

Photo by Scott Blake on Unsplash

Experimenting with implementing the fluent builder pattern in JavaScript.

The fluent builder pattern is a composition of the builder pattern and the fluent interface pattern.

It’s a pattern that holds our hand through the maze of object construction.

Our implementation uses es6 classes to give us something resembling a fluent builder.

Traditionally fluent interfaces are built using… interfaces.

Vanilla JavaScript doesn’t have interfaces. We’re left to do what we can with what we have.

(This is where someone says something about TypeScript. Have at it, but I never said I was writing about TypeScript. However, I would be delighted to see someone implement their own Fluent Builder in TypeScript or your language of choice)

For the curious, here is my attempt at implementing the pattern using JSDOC interfaces. I changed approaches after I realize that editor behavior was different between implementations.

How to build a burrito

To get to where we’re going first we will have to take a look at the builder pattern.

Wikipedia summarizes the pattern as

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.

That’s right. We are about to attempt to apply an object-oriented design pattern from a book[1] written in 1984 to JavaScript in 2020. What a time to be alive!

Anyway…

Maybe we want to make a burrito… Relax, this isn’t a monad tutorial

/**
 * Everyone loves Burritos
 */
class Burrito {
/**
 * @param {string} protein
 * @param {string} carb
 * @param {?string} salsa
 * @param {?string} cheese
 */
constructor(protein, carb, salsa, cheese) {
    // required toppings
    this.protein = protein;
    this.carb = carb;
    // optional toppings
    this.salsa = salsa;
    this.cheese = cheese;
  }
}

Enter fullscreen mode Exit fullscreen mode

Our take on a burrito has the following properties required in the constructor

  • a carb(ohydrate) such as brown or white rice
  • a protein such as shredded pork or beef

The following are optional (for whatever reason)

  • a salsa of some variety
  • cheese, queso, that ripe, runny, young or old fromage

Making (or constructing) a burrito as shown could look like this

const burrito = new Burrito(
  "brown rice",
  "shredded pork",
  "green salsa",
  "cojita"
);
// do stuff to the burrito

Enter fullscreen mode Exit fullscreen mode

If this burrito gets popular somehow we’re going to have to continue to make more and more burritos. Passing parameter after parameter in the same order to our Burrito.constructor [2]

We pass the parameters at the same time to construct the class instance.

To be annoyingly repetitive, using individual parameters got the job done, but have implications such as

  • all parameters must be passed at the same time
  • each parameter must be passed in the correct order
  • the constructor definition grows with each new parameter passed [3]

Now we will attempt to bypass these implications using a builder… (The burrito in the following snippet is the same one we looked at before.)

/**
 * Everyone loves Burritos
 */
class Burrito {
  /**
   * @param {string} protein
   * @param {string} carb
   * @param {?string} salsa
   * @param {?string} cheese
   */
  constructor(protein, carb, salsa, cheese) {
    // required toppings
    this.protein = protein;
    this.carb = carb;
    // optional toppings
    this.salsa = salsa;
    this.cheese = cheese;
  }
}

/*
 * BurritoBuilder adds flexibility to burrito construction
 */
class BurritoBuilder {
  constructor() {
    this.toppings = {}; // 1
  }

  // 2
  /**
   * Add a protein to burrito
   * @param {string} protein
   * @returns {BurritoBuilder}
   */
  withProtein(protein) {
    this.toppings.protein = protein;
    return this; // 3
  }

  /**
   * Add a carbohydrate to burrito
   * @param {string} carb
   * @returns {BurritoBuilder}
   */
  withCarb(carb) {
    this.toppings.carb = carb;
    return this;
  }

  /**
   * Add salsa to our burrito
   * @param {salsa} salsa
   * @returns {BurritoBuilder}
   */
  withSalsa(salsa) {
    this.toppings.salsa = salsa;
    return this;
  }

  /**
   * Add cheese to our burrito
   * @param {string} cheese
   * @returns {BurritoBuilder}
   */
  withCheese(cheese) {
    this.toppings.cheese = cheese;
    return this;
  }

  // 4
  /**
   * Wrap our toppings into a finished burrito
   * @returns {Burrito}
   */
  build() {
    const { protein, carb, cheese, salsa } = 
    this.toppings;
    return new Burrito(protein, carb, cheese, salsa);
  }
}

Enter fullscreen mode Exit fullscreen mode

There is a lot to unpack from our builder implementation! Let’s break down a few key points

  1. We store toppings in an object as a class property
  2. Topping adding methods follow the pattern of .with[ToppingName]
  3. We return a reference to the instance of the Burrito Builder after adding each ingredient
  4. Finally, we have a build method that will attempt to build a burrito using the toppings we selected. This method ties the room together by providing a tortilla wrapped resolution

Enough with the lists, time to put our BurritoBuilder to use!

const burrito = new BurritoBuilder()
  .withCarb("brown rice")
  .withSalsa("green")
  .withCheese("cojita")
  .withProtein("shredded pork")
  .build();

Enter fullscreen mode Exit fullscreen mode

In this example we’re passing all the ingredients at once. We’re able to build a burrito in one statement by method chaining. Method chaining is one flavor found in builders and is available because we return a reference to the builder in every method besides the finalizing build. (The return this in each chain-able method allows us to chain, but we are still free to assign our burrito-to-be to a variable whenever we’d like.)

We could easily do something in the spirit of 2020 era popular “healthy fast food” burrito joints

class CarbStation {
  static addIngredient(burrito, ingredient) {
    return burrito.withCarb(ingredient);
  }
}

class GrillStation {
  static addIngredient(burrito, ingredient) {
    return burrito.withProtein(ingredient);
  }
}

class ExtraStation {
  static addIngredient(burrito, category, ingredient) {
    if (category === "salsa") {
      return burrito.withSalsa(ingredient);
    }

    if (category === "cheese") {
      return burrito.withCheese(ingredient);
    }
    throw new Error("We don't sell that here!");
  }
}

class Cashier {
// oops, no register logic, free burritos
  static pay(burrito) {
    return burrito.build();
  }
}

Enter fullscreen mode Exit fullscreen mode

Let’s recreate our burrito from before. Notice how we’re passing a burrito builder around from class to classso they can each add toppings with love and care. Construction of the burrito is delayed until we see fit.

// Warning, the following may offend you if you only speak const or point-free
const burritoBuilder = new BurritoBuilder(); // (reference #1)

let burritoWithCarb = CarbStation.addIngredient(burritoBuilder, "brown rice"); // (reference #2)
let burritoWithCarbAndProtein = GrillStation.addIngredient(
burritoWithCarb,
"shredded pork"
); // (reference #3)

ExtraStation.addIngredient(burritoWithCarbAndProtein, "guac", true);
ExtraStation.addIngredient(burritoWithCarbAndProtein, "salsa", "green salsa");
ExtraStation.addIngredient(burritoWithCarbAndProtein, "cheese", "cojita");
const readyToEatBurrito = Cashier.pay(burritoWithCarbAndProtein);

Enter fullscreen mode Exit fullscreen mode

Notice a few things here.

  1. We can reference our burrito mid-construction with chaining or by variable assignment
  2. We have 3 different variables (marked with comments) referencing the same thing
  3. BurritoBuilder#build must be called when we’re ready to finalize our burrito build out
  4. We passed around an incomplete burrito builder. We called methods that independently added their own modifications.

So far we have briefly explored the second component of the term “fluent builder.” In true LIFO fashion, we will now look at the “fluent” component.

Fluent interfaces

Martin Fowler suggests that the term “fluent interface” is synonymous with an internal domain specific language.

In a summary of Fowler’s post, Piers Cawley poetically describes the fluent interface as a way to “move [sic moving] object construction behind a thoughtful, humane interface."

Our implementation will be using classes to work around JavaScripts lack of interfaces.

Without further ado, let’s introduce a plot twist so we can try to construct burritos behind a thoughtful, humane “interface.”

A wild boss appears

You’re sitting at your keyboard when suddenly a wild boss appearsBoss > Your burrito code has been working for us so far but there’s a problem! When I presented the code to the client (Healthy Burrito Chain) they told us about some business rules we failed to discover in the original project specification!You > Oh no! Not surprise business rules!Boss > Instead of filing TPS reports on Saturday, you need to come in and make sure we enforce the following rules when creating burritos…

(The rules the boss give you are as follows)

  1. In order for a burrito to be built, it must have a carb and a protein. We cannot allow a burrito to be created without these ingredients.
  2. After the required ingredients are submitted, we must allow customers to either pay or add one or more extra ingredient.
  3. The extra ingredients are salsa, and cheese

Oh No you think. It’s going to be a long weekend….

Saturday rolls around

Instead of throwing out the decision to use the builder pattern for our burritos, maybe we can make some adjustments by making our builder fluent.

Another way to look at our new business model by translating our burrito shop into a finite state machine

fluent builder finite state machine

fluent builder finite state machine

Shut up and show me the code

Let us take our implementation, wrap it with some classes. Hopefully whatever comes out won’t make Mr. Fowler cringe.


We’ll start with a class that allows us to set the protein.

class ProteinSetter {
  /**
   * @param {BurritoBuilder} builder
   */
  constructor(builder) {
    // 1
    this.builder = builder;
  }

  /**
   * @param {string} protein
   * @returns {CarbSetter}
   */
  withProtein(protein) {
    // 2
    return new CarbSetter(this.builder.withProtein(protein));
  }
}

Enter fullscreen mode Exit fullscreen mode

Notes:

  1. Our ProteinSetter class takes our builder from before. We’re wrapping the existing builder class instead of replacing the implementation.
  2. We pass the builder to the CarbSetter class after choosing a protein.

The CarbSetter class looks like this

class CarbSetter {
  /**
   * @param {BurritoBuilder} builder
   */
  constructor(builder) {
    this.builder = builder;
  }

  /**
   * @param {string} carb
   * @returns {ExtraSetter}
   */
  withCarb(carb) {
    return new ExtraSetter(this.builder.withCarb(carb));
  }
}

Enter fullscreen mode Exit fullscreen mode

This class is pretty similar to the ProteinSetter we just saw. After the carb is set, we pass our builder along to the ExtraSetter.

Are you starting to see the pattern here? We return class instances to control the flow of burrito construction.

The ExtraSetter class looks like this

class ExtraSetter {
  /**
   * @param {BurritoBuilder} builder
   */
  constructor(builder) {
    this.builder = builder;
  }

  /**
   * @param {number} salsa
   * @returns {ExtraSetter}
   */
  withSalsa(salsa) {
    this.builder.withSalsa(salsa);
    return this;
  }

  /**
   * @param {string} cheese
   * @returns {ExtraSetter}
   */
  withCheese(cheese) {
    this.builder.withCheese(cheese);
    return this;
  }

  /**
   * @returns {Burrito}
   */
  wrapUp() {
    return this.builder.build();
  }
  }

Enter fullscreen mode Exit fullscreen mode

Just like the other classes that we’ve seen, except for one crucial detail. The ExtraSetter can complete a build.

Our extra setter can:

  1. Add optional toppings in any order
  2. Complete the construction of our tortilla wrapped master piece

This last class is our entry point to the fluent burrito builder work flow.

/**
 * FluentBuilder to use as a starting point
 */
class FluentBuilder {
  static onTortilla() {
    return new ProteinSetter(new BurritoBuilder());
  }
}

Enter fullscreen mode Exit fullscreen mode

Drum roll, please

Now for the moment we’ve all been waiting for…

We can use our Fluent Builder as follows

const burrito = FluentBuilder.onTortilla()
  .withProtein("a")
  .withCarb("brown rice")
  .withCheese("cojita")
  .wrapUp();

Enter fullscreen mode Exit fullscreen mode

This is valid usage. Most editors will guide us down this path. Unlike the BurritoBuilder we can only call the methods that were intentionally exposed at any particular stage.

Fluent Builder in action

Fluent Builder in action

We’re forced down the happy path.

Go ahead, try it. Try to create a burrito using the FluentBuilder methods without adding a protein. That’s right, you can’t without directly accessing the builder (which is totally cheating)

I love it, how can I use it?

Personally I’ve been using Fluent Builders to constrain the construction of DTOs in tests and the application layer.

Feedback

Yes please @teh2mas


[1] https://en.wikipedia.org/wiki/Design_Patterns

[2] A common pattern JavaScript is to pass multiple parameters into a class constructor, method, or function as an object like

class Burrito({ carb, protein, salsa, cheese }) { /* ... */ }

Enter fullscreen mode Exit fullscreen mode

Which is a fine way of taking advantage of destructuring. We are also free to pass the parameters in whichever order we’d like.

[3] This can be a code smell hinting at a chance to decompose our class into smaller components

Top comments (0)