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;
}
}
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
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);
}
}
There is a lot to unpack from our builder implementation! Let’s break down a few key points
- We store toppings in an object as a class property
- Topping adding methods follow the pattern of
.with[ToppingName]
- We return a reference to the instance of the Burrito Builder after adding each ingredient
- 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();
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();
}
}
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);
Notice a few things here.
- We can reference our burrito mid-construction with chaining or by variable assignment
- We have 3 different variables (marked with comments) referencing the same thing
-
BurritoBuilder#build
must be called when we’re ready to finalize our burrito build out - 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)
- 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.
- After the required ingredients are submitted, we must allow customers to either pay or add one or more extra ingredient.
- 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
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));
}
}
Notes:
- Our
ProteinSetter
class takes our builder from before. We’re wrapping the existing builder class instead of replacing the implementation. - 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));
}
}
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();
}
}
Just like the other classes that we’ve seen, except for one crucial detail. The ExtraSetter
can complete a build.
Our extra setter can:
- Add optional toppings in any order
- 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());
}
}
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();
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
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 }) { /* ... */ }
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)