How are Fluent APIs made?

Crafting a Fluent API by hand is tricky for anything other than the simplest of cases. Fluent API Generator makes this much easier, but it can still be helpful to have an understanding of how a Fluent API is put together, and why they have the limitations that they do.
Some Fluent API code, floating through the ether.

Building a Pseudo-Fluent API

Let's start off with a Pseudo-Fluent API, as is it is a good springboard from which we can evolve our design to a full Fluent API.

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 this part of the What page.

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

Skip article

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 a static 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 than this.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

Skip article

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 the section above on static methods if you want to know more.

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 CakeMaker instance could still be created using the 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, but we couldn't follow this with any further methods.

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, as described above, it doesn't return anything.

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

Skip article

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;
}
The interface has been named 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 returns void.
  • 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 the PairOfTweezers 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");
	}
}
Hopefully you are familiar with The Swiss Army Knife, the essential tool used everywhere from camping trips to NASA space missions.

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() and saw() 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 allows us to use some methods in any order. Interfaces allow us to fix this by selectively making individual methods available at the right time.

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 the bake() 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 the PermitEat interface.
    • This means the class must have an eat() method that returns nothing (which it does).
  • Instead of returning a CakeMaker, the bake() method now returns a PermitEat.
    • Well, it is still a CakeMaker under the bonnet, but it is returned as a PermitEat.

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 a PermitEat.
  • the only thing a PermitEat allows is eat().
  • therefore, the only thing that can follow bake() is eat().

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 a PermitBake, allowing only bake() to be called
  • a bake() method that returns a PermitEat, allowing only eat() 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 the PermitBake interface in addition to PermitEat.
  • The mix() method now returns its created CakeMaker with a type of PermitBake instead of CakeMaker.

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.