How are Fluent APIs made?
![Some Fluent API code, floating through the ether.](/_app/immutable/assets/fluent-interface-coders.B_IP70hm.png)
Building a Pseudo-Fluent API
Let's start off with a
The examples below use TypeScript. The same principles apply to other OOP languages, such as Java & C#.
The 'CakeMaker' class
The first step is to create a simple class with a single method:
class CakeMaker {
gather() {
console.log(
"Gathering ingredients");
}
}
We can create an instance of this class, using new
, and call the
method:
new CakeMaker().gather();
// Output: Gathering ingredients
Adding a second method
Adding a second method to our CakeMaker class is straightforward:
class CakeMaker {
gather() {
console.log(
"Gathering ingredients");
}
prepare() {
console.log(
"Preparing ingredients");
}
}
And, now, we can call either method:
const cakeMaker = new CakeMaker();
cakeMaker.gather();
// Output: Gathering ingredients
cakeMaker.prepare();
// Output: Preparing ingredients
We now have a class with two methods. This is all well and good, but how can we start adding the fluency that a Fluent API is famed for?
We want to be able to do this:
new CakeMaker()
.gather()
.prepare();
Adding the fluency
The way we turn a method into a fluent method is to get it to return itself, using the this
keyword:
class CakeMaker {
gather() {
console.log(
" - Gathering ingredients");
return this;
}
prepare() {
console.log(
" - Preparing ingredients");
}
}
... which enables us, finally, to chain the two methods, one after the other:
new CakeMaker()
.gather()
.prepare();
// Output:
// - Gathering ingredients
// - Preparing ingredients
What is actually happening here?
- A CakeMaker instance is created, using
new CakeMaker()
. - The
gather()
method is called, which:- emits 'Gathering ingredients' to the console.
- returns
this
, which is the very same CakeMaker instance that has just been created.
- The
prepare()
method is then called on the returned CakeMaker, issuing 'Preparing Ingredients' to the console.
It might be helpful to consider this code:
const cakeMaker = new CakeMaker();
const theSameCakeMaker = cakeMaker.gather();
// Output: - Gathering ingredients
Here, we have called the gather()
method, causing 'Gathering ingredients'
to be written to the console, as before.
But, we have then assigned what is returned to a variable called theSameCakeMaker
.
And, we can verify that what was returned really is the same class instance:
console.log(cakeMaker === theSameCakeMaker);
// Output: true
Furthermore, we can still call prepare()
on the returned CakeMaker instance,
even though we're no longer calling it as part of a chain:
theSameCakeMaker.prepare();
// Output: - Preparing ingredients
Finishing up by adding the remaining methods
Now we have the basic principle in place, adding further methods is simple:
class CakeMaker {
gather() {
console.log(
" - Gathering ingredients");
return this;
}
prepare() {
console.log(
" - Preparing ingredients");
return this;
}
mix() {
console.log(
" - Mixing ingredients");
return this;
}
bake() {
console.log(
" - Baking the cake");
return this;
}
eat() {
console.log(
" - Eating the cake");
}
}
... which means we can now do this:
new CakeMaker()
.gather()
.prepare()
.mix()
.bake()
.eat();
// Output:
// - Gathering ingredients
// - Preparing ingredients
// - Mixing ingredients
// - Baking the cake
// - Eating the cake
Note that this
is returned each time, allowing the next CakeMaker method
to be called immediately, thus building up a chain of method calls.
You may have noticed that the eat()
method does not return this
. We will revisit that below, very shortly.
And that is it. We have a Pseudo-Fluent API!
Journey to a 'full' Fluent API - first steps
What is wrong with what we have, so far? Well, although a Pseudo-Fluent API allows us to chain methods together, it is also a free-for-all.
The finished example above would allow all these, none of which would lead to a good cake:
new CakeMaker().gather().prepare();
// The cake isn't finished ...
new CakeMaker().eat();
// We can't eat the cake without making it first ...
new CakeMaker()
.eat().bake().mix()
.prepare().gather();
// It's all backwards ...
How can we start to add the rules that restrict which methods are allowed to follow others, which is the hallmark of a 'true' Fluent API?
We will tackle the necessary steps, one by one. And, as it turns out, we've already tackled the first...
Adding the Executing Method
The following sections uses terms such as Executing Method and
Initiating Method. For an explanation of what these mean, have a read through
You may have noticed that, in our finished Pseudo-Fluent API example above, the final eat
method does not return this
. Simply doing this has given us our
Executing Method.
For example, this would not be allowed:
new CakeMaker()
.gather()
.prepare()
.mix()
.bake()
.eat()
.bake();
As the eat
method does not return this
, there
is nothing off which to chain a further method. Our code editor realises this and guides us with
a red underline.
Adding the Initiating Method
Adding the Initiating Method requires the use of a Static Method. If you are not familiar with what this means, the following article gives an overview (skip it if you're already familiar with Static Methods):
Static verses Instance Methods
The difference between Static and Instance Methods is a fundamental of Object-Oriented Programming and a deep dive is beyond the scope of this article.
However, there are some key comparison points that will help with understanding how Static Methods are used when building Fluent APIs:
Instance Methods | Static Methods |
---|---|
Belong to an individual object instance. | Are shared between all object instances created from the same class. |
Are only available on an object that has been created, from a class, using the new keyword. | Are available directly on the class itself; new is not needed. |
Operate on state that is held within each object instance. A new, independent state is created every time a new object is created. | Operate on a single class state that is shared between all object instances. If this state is changed by one object, that change is available to all other objects created from this class. |
Refer to state within the same object instance using a this prefix. | Refer to state shared between all objects using the Class Name as a prefix. |
We will now look at a simple example of each:
Instance Method example
Let's write a simple class that has a single, private variable and two methods for setting and getting it:
class ColourStore {
private colour: string = "";
setColour(colour: string) {
this.colour = colour;
}
getColour(): string {
return this.colour;
}
}
We can now create two object instances from this class, and set the colour for each:
const colourStore1 = new ColourStore();
colourStore1.setColour("blue");
const colourStore2 = new ColourStore();
colourStore2.setColour("green");
Note that we have to use new
each time we create a new instance.
And, now, we can use the getter method on each instance to return the colour state stored within:
console.log(
colourStore1.getColour());
// Outputs: 'blue'
console.log(
colourStore2.getColour());
// Outputs: 'green'
This demonstrates that the two object instances, although created from the same class, are completely independent of each other in terms of the colour state they hold.
Static Method example
Now, let's look at an equivalent that uses static methods:
class ColourStore {
private static colour: string = "";
static setColour(colour: string) {
ColourStore.colour = colour;
}
static getColour(): string {
return ColourStore.colour;
}
}
There are some points to note, here:
- The private
colour
variable now has astatic
modifier. - The methods for setting and getting are also adorned with
static
. - The
colour
variable is now referenced from within the class using the class name,ColourStore.colour
, rather thanthis.colour
.
Consequentially, the way colour
is set and retrieved also changes:
ColourStore.setColour("red");
console.log(
ColourStore.getColour());
// Outputs: 'red'
Note that we can now call the get and set methods directly on the class; there is no longer
any need to first create an object instance from the class using the new
keyword.
Combining the two
It is possible to have both Instance and Static methods on the same class. Seeing them working alongside each other can really help cement the understanding of how they differ.
In the following example:
- The variable name
colour
will be used for anything that involves an Instance method. - The variable name
sharedColour
will be used for anything that involves Static methods.
class ColourStore {
private colour: string = "";
private static sharedColour: string = "";
setColour(colour: string) {
this.colour = colour;
}
getColour(): string {
return this.colour;
}
static setSharedColour(colour: string) {
ColourStore.sharedColour = colour;
}
static getSharedColour(): string {
return ColourStore.sharedColour;
}
}
Now, we can compare how setting the property differs for each case:
// STATIC
// Set SharedColour using a static method:
ColourStore.setSharedColour("red");
// INSTANCE
// Create instances...
const colourStore1 = new ColourStore();
const colourStore2 = new ColourStore();
// ...and then set Colour using instance methods:
colourStore1.setColour("blue");
colourStore2.setColour("green");
... and, we can show the same comparison for getting a property:
// STATIC
console.log(
ColourStore.getSharedColour());
// Outputs: 'red'
// INSTANCE
console.log(
colourStore1.getColour());
// Outputs: 'blue'
console.log(
colourStore2.getColour());
// Outputs: 'green'
Lastly, we can see what happens if we try to use Static and Instance methods wrongly:
// Static method available directly on class:
ColourStore.setSharedColour("red");
// Instance method cannot be called similarly:
ColourStore.setColour("blue");
// Create instance...
const colourStore1 = new ColourStore();
// Instance method available on instance:
colourStore1.setColour("blue");
// Static method not available on instance:
colourStore1.setSharedColour("red");
You may now be wondering: what's the point of all this?
We are about to see how a combination of Instance and Static behaviours is foundational for creating the Initiating Method of a Fluent API.
To turn gather()
into an Initiating Method we need to:
- apply the
static
modifier to it - remove
return this
class CakeMaker {
static gather() {
console.log(
" - Gathering ingredients");
// return this;
}
... // Other methods
eat() {
console.log(
" - Eating the cake");
}
}
What does this do?
By declaring a method as static
we are saying it can be used directly on
the class, without the need to first create an instance object with new
:
CakeMaker.gather();
// Output:
// - Gathering ingredients
And, given that gather()
is the only method we have made static
, trying to use any other method directly on the class gives us errors:
CakeMaker.gather();
// Output:
// - Gathering ingredients
CakeMaker.prepare();
So, we now have an Initiating Method! But, we are not done, yet, as we have two new issues:
Method chaining is broken
What happens if we try to follow our Initiating Method with another method? It no longer works!
CakeMaker
.gather()
.prepare();
We can still call other methods first
And, we are still able to instantiate a CakeMaker
instance with new
, after which we can call and chain all the other methods, just as before:
new CakeMaker()
.prepare()
.mix()
.bake()
.eat();
Fortunately we can address these issues by use of a Private Constructor.
Introducing a Private Constructor
If you are not familiar with Constructors then the following article will help (skip it if you're already comfortable with them).
Constructors
As with Static Methods above, Constructors are a fundamental of Object-Oriented Programming and a deep dive is beyond the scope of this article. Nevertheless, we can cover enough of the basics to help us understand how they help when building a Fluent API.
Consider the following class:
class SimplePrinter {
private forPrinting = "something";
print() {
console.log(this.forPrinting);
}
}
We can create an object instance from this class and then call its print()
method, like so:
const mySimplePrinter = new SimplePrinter();
mySimplePrinter.print();
// Outputs: 'something'
But, suppose we wanted to give the instance some information as part of its creation? This is where the Constructor comes in:
class SimplePrinter {
private forPrinting;
constructor(initialValue: string) {
this.forPrinting = initialValue;
}
print() {
console.log(this.forPrinting);
}
}
The code in the Constructor runs when an object is created, and only once for each object.
In this case, the Constructor takes a parameter, initialValue
. The
code in the Constructor then assigns the parameter value to a private property called forPrinting
.
Now, we can pass some data in when the instance is created:
const mySimplePrinter =
new SimplePrinter("something else");
mySimplePrinter.print();
// Outputs: 'something else'
Indeed, because the constructor has a parameter, initialValue
, we
have to pass in some data. If we don't, we get an error:
const mySimplePrinter =
new SimplePrinter();
The private keyword
In the examples we have looked at so far, you may have noticed that some elements within our
classes have been marked private
.
Take the following class which has two properties, one public and one private:
class MyClass {
myPublicProperty;
private myPrivateProperty;
... // rest of class
Note: when using TypeScript, a property is public, by default, unless it is
marked as private
.
From outside the class, we can only access the public property:
const myObject = new MyClass();
// This property is accessible externally:
console.log(
myObject.myPublicProperty);
// This property is not:
console.log(
myObject.myPrivateProperty);
This is an example of encapsulation, another fundamental principle of Object-Oriented Programming. It means that you can control which aspects of your class are exposed to other code.
The internal workings of the class can be marked as private
and therefore
kept hidden, out of sight of everything else. Anything not marked private remains accessible by
code external to the class.
Private constructors
It is also possible to make the Constructor private
:
class SimplePrinter {
private forPrinting;
private constructor() {}
... // rest of class
... and, having done this, it is no longer possible to create object instances, as the constructor is no longer accessible from the outside world:
const mySimplePrinter =
new SimplePrinter();
However, although the constructor can no longer be accessed from outside the class,
it can still be used from the inside. This means the responsibility for creating new
objects can be given to a dedicated create()
method:
class SimplePrinter {
private forPrinting;
private constructor() {
}
static create() {
return new SimplePrinter();
}
}
And, now, we have a new way to create object instances:
// Private Constructor still prevents this:
const mySimplePrinter =
new SimplePrinter();
// But, now, this is allowed:
const mySimplePrinter =
SimplePrinter.create();
Note that the create()
method is static
,
meaning it can be called directly on the class rather than on an instantiated object.
Refer back to
Why would we want to do this? Private Constructors do have a number of uses, which we won't go into here.
We are particularly interested in how a Private Constructor helps us build a Fluent API. This is explained next.
We have two competing needs, here:
- We want to have a single
static
Initiating Method on our class, such that it has to be the first method used. - But, we also want to retain instance behaviour for the chained methods that come after this.
A Private Constructor, and the use of it from within the class, allows us to combine these asks:
class CakeMaker {
private constructor() {}
static gather() {
console.log(
" - Gathering ingredients");
return new CakeMaker();
}
prepare() {
console.log(
" - Preparing ingredients");
return this;
}
... // Other methods
eat() {
console.log(
" - Eating the cake");
}
}
How does this solve our issues?
Chaining methods off a 'new' CakeMaker is no longer possible
Even though we had successfully created an Initiating Method, we had the issue
that a
new
keyword. This is no longer possible:
new CakeMaker()
.prepare()
.mix()
.bake()
.eat();
Chaining from the Initiating Method now works
Previously, we could call the static gather()
static method directly off
the CakeMaker class,
Now, we can:
CakeMaker
.gather()
.prepare()
.mix()
.bake()
.eat();
Why is this? Well, even though the private constructor means the static gather()
method is the only one we can call from outside the class, this does not mean that a new CakeMaker()
cannot be created, and returned, from within the class. Or, more specifically, from within the
static gather()
method:
class CakeMaker {
private constructor() {}
static gather() {
console.log(
" - Gathering ingredients");
return new CakeMaker();
}
... // Other methods
}
So, gather()
returns a 'new' CakeMaker instance which allows methods to
be chained, as per our original Pseudo-Fluent API. But, unlike with the Pseudo-Fluent API, calling
gather()
, first, is the only way to get this instance.
A recap of where we have got to
We have come quite a way already. We now have a class structured with the following characteristics:
- It has a number of methods which can be chained, one after the other.
- It has an Initiating Method, which has to be called first.
- It has an Executing Method, which has to be called last.
// Starting with anything other than `gather()` causes an error...
CakeMaker
.prepare()
.gather()
.mix()
.bake()
.eat();
// Finishing with anything other than `eat()` causes an error...
CakeMaker
.gather()
.prepare()
.mix()
.eat()
.bake();
This is good progress! However, we still have an issue.
An ordering issue remains
Our Initiating and Executing methods are sorted, but the intermediary methods,
prepare()
, mix()
and bake()
, can still be called in any order:
// This is good...
CakeMaker
.gather()
.prepare()
.mix()
.bake()
.eat();
// This is bad: prepare(), mix() and bake() are allowed to go in the wrong order...
CakeMaker
.gather()
.mix()
.bake()
.prepare()
.eat();
To deal with this, we need to move things up a gear, by introducing Interfaces.
Next steps - controlling method order with Interfaces
Up until this point we have not used much explicit typing in our code. Indeed, although these examples have been written with TypeScript in mind, everything described, so far, might as well have been written in JavaScript.
That's about to change.
Introducing types
JavaScript uses implicit typing. Consider the following code:
const myFirstVariable = "This is a string.";
const mySecondVariable = 7;
The first variable here is a string
. The second variable is a number
. These are two of the types that JavaScript uses (there are several others).
We haven't declared anywhere that these variables are a string and number. In each case, JavaScript works out what the variable type is by looking at the value assigned to it.
Further proof of these variable types can be seen here:
const myFirstVariable = "This is a string.";
const mySecondVariable = 7;
// Outputs: 'THIS IS A STRING':
console.log(myFirstVariable.toUpperCase());
// Causes an error:
console.log(mySecondVariable.toUpperCase());
The number
type does not have a toUpperCase()
method
on it, so our code editor highlights the mistake with a red underline.
It is pretty obvious what the types are in this simple example, but that is not always the case.
TypeScript allows us to be more explicit:
const myString: string = "This is a string.";
const myNumber: number = 7;
Why would we declare the types like this when we know JavaScript is doing a decent job working them out anyway? As it happens, for simple variables like this, some coding teams will choose not to add types.
So, we don't strictly need to add types at this point. But, doing so will help us set the
scene for using the interface
keyword shortly. When using interfaces, explicit
typing is mandatory; if we add typing to our existing code now, it will be easier to understand how
the interfaces are applied, thereafter.
Had we been doing these examples in Java or C#, we would have had to declare types from the start.
TypeScript is slightly unusual in this respect; is an explicit-typing layer that sits on top of implicitly-typed JavaScript but which, nevertheless, maintains a 'take it or leave it' approach.
Before proceeding further, we are going to strip our CakeMaker example back a bit. This will keep things simple while we introduce some new concepts (we will have restored CakeMaker to its full glory by the time we finish):
class CakeMaker {
private constructor() {
//
}
static mix() {
console.log(
" - Mixing ingredients");
return new CakeMaker();
}
bake() {
console.log(
" - Baking the cake");
return this;
}
eat() {
console.log(
" - Eating the cake");
}
}
Our lesser CakeMaker has only three steps:
- an Initiating Method of
mix()
, - a following method of
bake()
, - lastly, an Executing Method of
eat()
.
Now, consider the same code, updated with types:
class CakeMaker {
private constructor() {
//
}
static mix(): CakeMaker {
console.log(
" - Mixing ingredients");
return new CakeMaker();
}
bake(): CakeMaker {
console.log(
" - Baking the cake");
return this;
}
eat(): void {
console.log(
" - Eating the cake");
}
}
Each of the three methods now has a type annotation showing what the method returns:
CakeMaker
for the first two,- and
void
for the last.
We already know that mix()
returns a new CakeMaker
instance. Adding the type annotation explicity declares this.
Additionally, we know that the bake()
method returns
this
, which is the very same instance, also of type CakeMaker
.
The eat()
method is different. This is our
Executing Method
and,
To indicate this with typing, we use the void
type annotation, which means
nothing is being returned.
What use is this? That should become very apparent once we start digging into Interfaces below, but we can see an immediate benefit very simply.
What happens if, for our mix()
method, we try to return something other
than a CakeMaker
? Our Code Editor will warn us:
class CakeMaker {
private constructor() {
//
}
static mix(): CakeMaker {
console.log(
" - Mixing ingredients");
return "This is a string, not a CakeMaker";
}
... // Other methods
}
Or, suppose we forget to return anything at all?
class CakeMaker {
private constructor() {
//
}
static mix(): CakeMaker {
console.log(
" - Mixing ingredients");
// return new CakeMaker();
}
... // Other methods
}
Again, we get a clear warning that something is amiss. Straight away, the typing we have added has protected us from making some simple, yet easy, mistakes in our code.
Returning an Interface that limits what comes next
If you are not familiar with Interfaces then have a read through the article below. But do skip it if it is just going over old ground.
Interfaces
As per the previous articles on this page, Interfaces are a fundamental of Object-Oriented Programming and a deep dive is beyond the scope of this article. Nevertheless, we can cover enough of the basics to help us understand how they help when building a Fluent API.
What is an Interface?
Perhaps the best way to understand them is by example. Consider the following class, for a pair of tweezers:
class PairOfTweezers {
tweeze(): void {
console.log(
" - Using tweezers");
}
}
Now, suppose you find a splinter in your finger, which you want to remove. Can you use a PairOfTweezers
for this? Well, yes:
const tweezers: PairOfTweezers =
new PairOfTweezers();
tweezers.tweeze();
// - Using tweezers
What if we wanted to represent this 'tweeze' functionality with an Interface? The code to do so would look like this:
interface ICanTweeze {
tweeze(): void;
}
ICanTweeze
, to show that anything which
has this interface is guaranteed to be able to 'tweeze'.There are some points to note regarding this interface:
- It has a
tweeze()
method signature, which returnsvoid
. - The interface only contains a method signature, indicating the name of the
method, what parameters it takes (none, in this case) and what it returns (
void
, aka. nothing). There is no code here that actually does anything. - The interface has the same method signature as the
tweeze()
method in thePairOfTweezers
class.
Implementing an Interface
A class which uses an Interface is said to implement it. Here is the PairOfTweezers
class implementing the ICanTweeze
interface:
class PairOfTweezers implements ICanTweeze {
tweeze(): void {
console.log(
" - Using tweezers");
}
}
And, now the class implements the ICanTweeze
interface, it
has
to obey it. This is what happens if we comment out the tweeze()
method:
class PairOfTweezers implements ICanTweeze {
// tweeze(): void {
// console.log(
// " - Using tweezers");
// }
}
The interface says that PairOfTweezers
must have a tweeze()
method; if we removed it, the code editor tells us that something is wrong.
What is more, we can create a variable that uses an interface type rather than a class type:
const tweezers: ICanTweeze =
new PairOfTweezers();
tweezers.tweeze();
// - Using tweezers
The object assigned to the tweezers
variable is still an instance of PairOfTweezers
. But, it is now typed as ICanTweeze
.
This ability to have the same object instance represented as different types is called polymorphism.
Using polymorphism to reveal behaviour selectively
So far, we have seen a class implementing the ICanTweeze
interface. But,
this doesn't stop us adding additional methods.
Let's look at an example that still 'tweezes', but does some other stuff, too:
class SwissArmyKnife implements ICanTweeze {
tweeze(): void {
console.log(
" - Using tweezers");
}
cut(): void {
console.log(
" - Cutting stuff");
}
saw(): void {
console.log(
" - Sawing stuff");
}
}
This SwissArmyKnife
class can be instantiated and assigned to a variable
of type SwissArmyKnife
as follows:
const swissArmyKnife: SwissArmyKnife =
new SwissArmyKnife();
swissArmyKnife.tweeze();
// - Using tweezers
swissArmyKnife.cut();
// - Cutting stuff
swissArmyKnife.saw();
// - Sawing stuff
However... as with the PairOfTweezers
class, it implements the ICanTweeze
interface, so it can be assigned to a variable of that type, too:
const swissArmyKnife: ICanTweeze =
new SwissArmyKnife();
swissArmyKnife.tweeze();
// - Using tweezers
swissArmyKnife.cut();
swissArmyKnife.saw();
... but wait! what is this? Now we see errors indicated when trying to call the cut()
and saw()
methods.
And this gets to the heart of Polymorphism; we are dealing with an instance of SwissArmyKnife
in both cases, but:
- When assigned to a variable of type
SwissArmyKnife
: tweeze()
,cut()
andsaw()
methods are available.- When assigned to a variable of type
ICanTweeze
: - only the
tweeze()
method is available.
You may remember the issue we want to solve for our Fluent API; it still
How Polymorphism promotes flexibility
Let's finish off this whirlwind tour of interfaces with a final example showing the flexibility they can bring.
Returning to our story about fingers, splinters and tweezers: suppose there is a First Aider on the scene who is going to help you remove the splinter from your finger.
A corresponding FirstAider
class might look like this:
class FirstAider {
private tweezers: ICanTweeze;
constructor(tweezers: ICanTweeze) {
this.tweezers = tweezers;
}
extractSplinter() {
console.log("Hold still!");
this.tweezers.tweeze();
}
}
This FirstAider
class has a constructor, which takes an ICanTweeze
instance. This ICanTweeze
instance is then stored within the class as
the
tweezers
property. Finally, there is an extractSplinter()
method, that uses the
ICanTweeze
instance.
As a consequence, we now have a very flexible First Aider! We can give them either:
... a Pair Of Tweezers:
const tweezers: ICanTweeze =
new Tweezers();
const firstAider =
new FirstAider(tweezers);
firstAider.extractSplinter();
// Hold still!
// - Using tweezers
... or, a Swiss Army Knife:
const swissArmyKnife: ICanTweeze =
new SwissArmyKnife();
const firstAider =
new FirstAider(swissArmyKnife);
firstAider.extractSplinter();
// Hold still!
// - Using tweezers
The point is the First Aider doesn't care if you give them a Swiss Army Knife or a pair of tweezers; all they care about is that they have a tool that can 'tweeze'.
Both PairOfTweezers
and SwissArmyKnife
guarantee
this, because they both implement the ICanTweeze
interface.
Let's now return to building out our Fluent API, using Interfaces to control how methods are chained.
Considering our scaled-back Fluent API further, we know that we want:
- our Initiating Method,
mix()
, to be followed by thebake()
method only, - the
bake()
method to then be followed only by our Executing Method,eat()
.
Focussing on the second of these requirements first: we can create such a restriction
by means of an Interface that permits only the eat()
method to be called:
interface PermitEat {
eat(): void;
}
If we get the bake()
method to return a PermitEat
, then we know eat()
is the only method that can be called on it.
Let's try that:
interface PermitEat {
eat(): void;
}
class CakeMaker implements PermitEat {
private constructor() {
//
}
static mix(): CakeMaker {
console.log(
" - Mixing ingredients");
return new CakeMaker();
}
bake(): PermitEat {
console.log(
" - Baking the cake");
return this;
}
eat(): void {
console.log(
" - Eating the cake");
}
}
There are a few changes, here:
- We now have both the interface and the class written out together. This is convenient for seeing how they work with each other.
- The
CakeMaker
class now implements thePermitEat
interface.- This means the class must have an
eat()
method that returns nothing (which it does).
- This means the class must have an
- Instead of returning a
CakeMaker
, thebake()
method now returns aPermitEat
.- Well, it is still a
CakeMaker
under the bonnet, but it is returned as aPermitEat
.
- Well, it is still a
Where has this got us? Let's try it out and see.
Method chaining works as it did before:
CakeMaker.mix().bake().eat();
But, now, if we try to follow bake()
with anything other than eat()
, we get an error:
CakeMaker.mix().bake().mix();
Before we added the interface, this error would not have shown. But now:
bake()
is returning aPermitEat
.- the only thing a
PermitEat
allows iseat()
. - therefore, the only thing that can follow
bake()
iseat()
.
Hopefully you can now see how interfaces are critical for making a Fluent API work.
The final piece of the puzzle
We're nearly there, although there is one more issue to solve.
Both these are still possible, whereas only the first should be:
// This is fine and should be allowed:
CakeMaker.mix().bake().eat(); ✔️
// This should not be allowed; only `bake()` should follow `mix()`:
CakeMaker.mix().eat(); ❌
As things are currently, the Fluent API lets us go directly from mix()
to
eat()
, missing out bake()
altogether.
How can we prevent this? As you may have guessed, we can create another interface:
interface PermitBake {
bake(): PermitEat;
}
This is very similar to the PermitEat
interface that we created earlier.
But, there is one crucial difference:
Rather than enforcing a return type of void
(or 'nothing'), it is instead
returning a PermitEat
!
This does make sense. It means we can have:
- a static
mix()
method that returns aPermitBake
, allowing onlybake()
to be called - a
bake()
method that returns aPermitEat
, allowing onlyeat()
to be called
This is Polymorphism in action; for different steps we are returning a CakeMaker
'disguised' as different interfaces, PermitBake
or PermitEat
, depending on what step we want to allow next.
Let's see how it looks:
interface PermitBake {
bake(): PermitEat;
}
interface PermitEat {
eat(): void;
}
class CakeMaker implements
PermitBake,
PermitEat {
private constructor() {
//
}
static mix(): PermitBake {
console.log(
" - Mixing ingredients");
return new CakeMaker();
}
bake(): PermitEat {
console.log(
" - Baking the cake");
return this;
}
eat(): void {
console.log(
" - Eating the cake");
}
}
Three changes have been made:
- The
PermitBake
interface has been added at the top. CakeMaker
now implements thePermitBake
interface in addition toPermitEat
.- The
mix()
method now returns its createdCakeMaker
with a type ofPermitBake
instead ofCakeMaker
.
As a consequence, the only thing that should be able to follow mix()
is
bake()
.
Let's try it out:
// This is fine and should be allowed:
CakeMaker.mix().bake().eat();
// This should not be allowed; only `bake()` should follow `mix()`:
CakeMaker.mix().eat()
As expected, if we try to follow mix()
with
eat()
, we see an error.
And that's it. We now have a Fluent API!
What next?
Hopefully, you can now see that, by repeating the same technique, we could extend CakeMaker
to have additional methods, such as the gather()
and prepare()
methods that we removed from our Pseudo-Fluent API example, all that time ago.
This would give us a Fluent API that allows:
CakeMaker
.gather()
.prepare()
.mix()
.bake()
.eat();
... and it would guarantee that these are called in this order, and this order only.
We would need to create PermitPrepare
and PermitMix
interfaces. And, we would need to adjust the class methods to use them.
This is how the code for such a Fluent API would look:
interface PermitPrepare {
prepare(): PermitMix;
}
interface PermitMix {
mix(): PermitBake;
}
interface PermitBake {
bake(): PermitEat;
}
interface PermitEat {
eat(): void;
}
class CakeMaker implements
PermitPrepare,
PermitMix,
PermitBake,
PermitEat {
private constructor() {
//
}
static gather(): PermitPrepare {
return new CakeMaker();
}
prepare(): PermitMix {
return this;
}
mix(): PermitBake {
return this;
}
bake(): PermitEat {
return this;
}
eat(): void {
//
}
}
A slightly more complex example
Already, this is starting to get a bit involved.
But, suppose we wanted to do something just a little more sophisticated? Repeating the mix()
and bake()
methods more than once, for example:
CakeMaker
.gather().prepare()
.mix().mix()
.bake().bake()
.eat();
A Fluent API can be made that allows exactly that. This is what it looks like:
interface PermitPrepare {
prepare(): PermitMix;
}
interface PermitMix {
mix(): PermitMixOrBake;
}
interface PermitMixOrBake {
mix(): PermitMixOrBake;
bake(): PermitBakeOrEat;
}
interface PermitBakeOrEat {
bake(): PermitBakeOrEat;
eat(): void;
}
class CakeMaker implements
PermitPrepare,
PermitMix,
PermitMixOrBake,
PermitBakeOrEat {
private constructor() {
//
}
static gather(): PermitPrepare {
return new CakeMaker();
}
prepare(): PermitMix {
return this;
}
mix(): PermitMixOrBake {
return this;
}
bake(): PermitBakeOrEat {
return this;
}
eat(): void {
//
}
With a bit of consideration, we can see what is happening, here. If we are to be able to repeat mix()
, for example, then we must permit mix()
to be followed by mix()
or bake()
. And, therefore, we need a PermitMixOrBake
interface that allows this.
As you can see from this example, an Interface can have more than one method within, so this is perfectly possible.
But, we also need a PermitBakeOrEat
interface to handle the repeating
bake()
method. And working out which method should return which interface
becomes less than obvious.
A much more complex example
Suppose we want to build a Fluent API that helps us write automation tests for an email app. We might want to do something like this:
const email = EmailBuilder
.addSubject(subject: string) // Can only be done once
.addContent(content: Html) // Can only be done once
.addTo(to: EmailAddress) // Can be done repeatedly
.addCc(cc: EmailAddress) // Can be done repeatedly
.addBcc(bcc: EmailAddress) // Can be done repeatedly
.build();
We want the email to have a Subject and some Content. And, we want to be able to send it to as many people as we like. And, some of those people can be 'Cc' (carbon copy) or 'Bcc' (blind carbon copy) instead of 'To'.
Here is what the Fluent API for this would look like. To make this a more full-featured example, each method has had a parameter added, along with some code in the method body:
interface PermitAddContent {
addContent(content: Html): PermitAddTo;
}
interface PermitAddTo {
addTo(to: EmailAddress): PermitAddToOrAddCcOrAddBccOrBuild;
}
interface PermitAddToOrAddCcOrAddBccOrBuild {
addTo(to: EmailAddress): PermitAddToOrAddCcOrAddBccOrBuild;
addCc(cc: EmailAddress): PermitAddCcOrAddBccOrBuild;
addBcc(bcc: EmailAddress): PermitAddBccOrBuild;
build(): Email;
}
interface PermitAddCcOrAddBccOrBuild {
addCc(cc: EmailAddress): PermitAddCcOrAddBccOrBuild;
addBcc(bcc: EmailAddress): PermitAddBccOrBuild;
build(): Email;
}
interface PermitAddBccOrBuild {
addBcc(bcc: EmailAddress): PermitAddBccOrBuild;
build(): Email;
}
class EmailBuilder implements
PermitAddContent,
PermitAddTo,
PermitAddToOrAddCcOrAddBccOrBuild,
PermitAddCcOrAddBccOrBuild,
PermitAddBccOrBuild {
private email: Email = new Email();
private constructor(subject: string) {
this.email.subject = subject;
}
static addSubject(subject: string): PermitAddContent {
return new EmailBuilder(subject);
}
addContent(content: Html): PermitAddTo {
this.email.content = content;
return this;
}
addTo(to: EmailAddress): PermitAddToOrAddCcOrAddBccOrBuild {
this.email.toList.push(to);
return this;
}
addCc(cc: EmailAddress): PermitAddCcOrAddBccOrBuild {
this.email.ccList.push(cc);
return this;
}
addBcc(bcc: EmailAddress): PermitAddBccOrBuild {
this.email.bccList.push(bcc);
return this;
}
build(): Email {
return this.email;
}
}
Trying to build such a Fluent API manually would be a real challenge.
Fortunately, Fluent API Generator makes this much, much easier. Indeed, EmailBuilder
is one of the Fluent API examples you'll find there.