Writing code is like creating art.
With years of experience, one starts to recognize the quality of code, and what best practices and patterns works best.
One of the popular patterns out there for helping developers to easily read code, is known as the fluent builder pattern, aka. builder pattern.
Fluent builder
It’s mostly useful when you need to create an object with lots of possible configuration options. You end up having an object that can dynamically be changed throughout its life-cycle.
In essence, the goal of the fluent builder is reduce number of arguments a constructors takes, and provide a flexible way of adding behavior to an object, check out the example below showing before/after fluent builder is applied:
// Before (no fluent builder)
const person1 = new Person('Peter', 26, true, 40074986, 4, 2);
// After (fluent builder applied)
const person1 = new Person();
person1
.name('Peter')
.age(26)
.member(true)
.phone(40074986)
.children(4)
.cars(2);
This pattern is not new, you’ve most likely encounter it in programming paradigms such as functional programming, object-relational mapping (ORM), and reactive programming where methods are chained like words in a sentence. This way, the developer can easily understand the content of the code by reading from left to right.
- Derfor lager vi Folq med Elm og Haskell
- Tjenesten krever en stabil backend, og en frontend som kan tilpasses kontinuerlig.
Topics we’ll cover:
- Why fluent builder?
- Launch rocket to space (example)
- The fluent builder way
- Summary
1. Why fluent builder?
The best way to explain something, is to show an example. For that reason, we’ll start of by showing a “bad” code example, then describe how it can be improved when using the fluent builder.
2. Launch rocket to space (example)
Let’s imagine we work at SpaceX, one of the projects we are assigned to work with is creating the SW responsible for launching a rocket to space. The goal is to create many options so that the engineers have full access of the rockets behavior, such as when to launch, set the speed, set the location and so forth.
Let’s create a Rocket class with these properties:
class Rocket {
engines: number;
speed: number;
location: string;
launch: boolean;
stop: boolean;
timer: number;
constructor(engines, speed, location, launch, stop, timer) {
this.engines = engines;
this.speed = speed;
this.location = location;
this.launch = launch;
this.stop = stop;
this.timer = timer;
}
}
Let’s build the rocket by creating a new object named falconHeavy. Take a moment to notice the number of arguments required in order to create the rocket object:
// A method with too many arguments
const falconHeavy = new Rocket(27, 17000, 'Mars', true, false, 120);
- Constructor has too many arguments
Too many arguments makes it difficult to remember the required order of the arguments. This is an antipattern known as telescoping issue. - Too many responsibilities
With 6 arguments it fails to satisfy the single responsibility principle (SRP). However, there are few use cases where this is fully normal, but ending up with many arguments should be enough to evaluate other alternatives. - Non-scalable
What if we only want to set few arguments such as the rockets location and time to launch?
Let’s now apply the fluent builder, and see how we can solve these challenges as explained above.
3. The fluent builder way
So how can we implement the fluent builder? Let’s compare the Rocket class with and without the fluent builder.
Fluent builder (not applied):
class Rocket {
engines: number;
speed: number;
location: string;
launch: boolean;
stop: boolean;
timer: number;
constructor(engines, speed, location, launch, stop, timer) {
this.engines = engines;
this.speed = speed;
this.location = location;
this.launch = launch;
this.stop = stop;
this.timer = timer;
}
}
Fluent builder (applied):
class Rocket {
speed: number;
location: string;
launch: boolean;
constructor() {
// fluent builder implemented
}
speed(speed: number) {
this.speed = speed;
}
location(location: string) {
this.location = location;
}
launch() {
this.launch = true;
}
}
As shown above, we’ve simply moved the properties out of the constructor, and then create a method for each one. This makes it possible to extend the behavior of the class along the way, instead of having to define all arguments in the constructor when we create a new Rocket class.
Doing so, we can chain methods like:
const falconHeavy = new rocket();
falconHeavy
.location('mars')
.speed(17000)
.launch();
As shown above, we end up with a declarative (easy-to-understand) approach where we can dynamically define the objects representation as we go. It also means we can use this in multiple places, for instance we may only want to set the location and time for the launch.
Hva er funksjonell programmering? 🤔
To norske eksperter forklarer, og lokker.
4. Summary
As with everything, don’t go crazy with the implementation of the fluent builder. Just be aware of when you end up with a constructor with many arguments, the fluent builder may be a good alternative.
Personally, I really like writing code where you have to chain methods in a declarative way. It makes it easy to work with, understand, and also scale. This style of programming is widely recognized, and a friendly approach for extending behavior when needed.
Resources:
You can find me on Medium where I publish on a weekly basis. Or you can follow me on Twitter.
- Programmering kan ikke gjøres på samlebånd!
- Jeg skjønner ikke at mange norske utviklere finner seg i å være “human keyboard”, sier Ukas Koder Christine Teig.