Why use a Fluent API?
Fluent APIs are everywhere
If you have been coding for anything more than a short while, it is very likely that you have already used a Fluent API. Here are some popular examples:
With a combination of versatility and extensibility, jQuery changed the way millions of people build websites.
$("#toast")
.addClass("info")
.html("Welcome!")
.fadeIn("slow");
LINQ
Language Integrated Query, available for .NET languages such as C#, allows for a consistent syntax to query and manipulate data from a range of different sources.
var result = data
.Where(x => x > 10)
.OrderBy(x => x)
.Select(x => x * 2);
Selenium WebDriver, a browser Automation Framework, uses a Fluent API to drive interaction with browsers and web pages.
driver
.findElement(
By.id("username"))
.sendKeys("text")
.submit();
Reasons to use a Fluent API
To make life easy for consumers
Why have these well-respected development teams chosen to use Fluent APIs?
Fluent APIs are especially useful if you are writing code that will be used by others, such as for an API or Framework, because they are so good at giving consumers context-sensitive guidance.
But you don't have to be writing an open source or commercial library to use them. Fluent APIs can be useful whenever your code will be used by others in their own coding journeys, be that the developer community, your team, or even you returning to old code, because:
- Consumers see typing and ordering issues immediately, while they are coding. They don't have to try running the code, or wait for Unit Tests to fail, in order to know there is something wrong.
Complexity is hidden, making the intended flow of your code super-clear. This is especially useful for those using your library for the first time.
The library is easier to use for those new to coding. For example, there is an increasing expectation that Quality Assurance teams should be able to write automation tests that perform actions on a product, just as a real user would. A Fluent API can be very helpful in keeping automation steps on the right path.
Besides, even if a QA has coding experience (and there are some very competent QA coders), a Fluent API can still make life easier. Fluent APIs are common in Test Frameworks for this very reason.
The code suits fluency
There are situations that suit Fluent APIs particularly well:
- You have one or more relatively simple sets of steps where order matters.
- The steps to be carried out are clearly defined and well compartmentalised.
- None of the limitations of Fluent APIs,
as described on the What page , get in the way.
Returning to the example libraries above (jQuery, LINQ and Selenium), you can see that they each fit a step-by-step pattern well, be that for manipulating a browser DOM, querying a database or driving a test automation framework.
Reasons not to use a Fluent API
Notice above the mention of limitations and the emphasis on relatively simple. If your code does not fit these guidelines then you should not be tying yourself up in knots, trying to crowbar a Fluent API into a codebase where it does not belong.
And, all limitations aside, it can be tempting to use Fluent APIs even if there is no real advantage to doing so, because they're really cool.
Although Fluent API Generator makes creating Fluent APIs simpler, they still introduce complexity into your code. Whilst Fluent APIs can make code easier to consume, they also make code harder to maintain.
There are a number of ways in which a Fluent API can be problematic:
They break Object-Oriented Programming principles
For example, objects created from a Fluent API use a private constructor, which prevents inheritance.
This is not an issue, as long as you are aware that you cannot treat a Fluent API as just another class, i.e. you are making a considered, architectural decision to use one.
You can find out more about Private Constructors on the
They are hard to maintain
By making life easier for consumers who use the library, you are making life harder for yourself, maintaining the library.
As you will see if you try out Fluent API Generator, the lines of code can build up quickly for anything but the simplest cases.
What is more, the code produced is not the easiest to read; there is a complex interplay between an Object-Oriented Class and at least one Interface. Indeed, at first glance, it is quite hard to work out what a Fluent API actually allows you to do.
The Examples section of Fluent API Generator gives a clear indication of what the Fluent API permits.
Fluent API Generator undoubtedly makes all this easier. Nevertheless, once you have populated your Fluent API steps with actionable code, it becomes more of a chore to change things around.
They can impact performance
In general, more lines of code take longer to execute. Furthermore, Object-Orientated code constructs, which Fluent APIs are, are not necessarily the most performant choice in and of themselves.
In most codebases, the impact a Fluent API has on execution speed is so small as to be meaningless. Nevertheless, if you were working on something performance-critical, e.g. trying to squeeze a few extra frames per second out of a game, you would not want to go anywhere near a Fluent API.
They are harder to Unit Test
Suppose we have a Fluent API which is used to query a database of users:
How would you go about unit testing this? The Users
class that starts everything off has a static, private constructor, so it is not possible to use
the constructor to pass in a mock user database for testing purposes.
One solution would be taking the responsibility of obtaining the user database instance away from the Fluent API. Instead, you would pass it in using a dedicated step at the start. Then, you could pass in either a 'real' user database or a mock:
... but, even having done this, we still have to consider carefully how we might write tests.
To continue with the example above, we might suppose that the different steps build up a string of SQL, which is then executed against the user database. Our mock DB could assert that it is being called with the correctly constructed SQL strings, depending on which set of steps has been carried out.
But what if the code that builds up the SQL is complex? If it is buried within a Fluent API step, how can it be accessed for Unit Testing in isolation?
The answer is to pull out any complex SQL generation code into one or more separate modules,
that DatabaseQueryMaker
depends upon. This way, the complex step code can
be unit tested in isolation.
(Breaking complexity out into individual, simpler modules is good coding practice, regardless.)
Furthermore, we could arrange the code such that DatabaseQueryMaker
has
no SQL construction responsibilities. Rather, these responsibilities are held in other code modules
and
DatabaseQueryMaker
becomes a simple proxy for passing method calls on.
A proxy, that simply passes method calls to something else, is much easier to Unit Test.
They are harder to mock
We have considered unit testing the Fluent API itself, but we also need to think about testing the code that uses it. For this, we would need to build a mocked Fluent API.
Unfortunately, this is quite a challenge! Two ways forward might be:
Have a 'real' instance of your Fluent API depend on a mock. With this approach, you would not mock the Fluent API. Rather, you would pass a mock to it.
To continue again with the example above, you would use a mock User Database for your 'real'
DatabaseQueryMaker
:And then, you can set up your mock to return test SQL strings that are passed through the Fluent API, and then on to the code modules that use
DatabaseQueryMaker
.This is not ideal, as you are relying on an intermediary component (the Fluent API) which sits between the mock User Database and the code under test. This is a problem particularly if the code for generating SQL within the Fluent API steps is complex.
Again, this will work best if your Fluent API internals are very simple, with any complexity farmed off to other modules.
Use a mocking library that supports Fluent APIs. Many mocking libraries support setting up methods to return the mock instance itself, just as a Fluent API does. Once again, though, this introduces complexity.
In summary, there is nothing here that is insurmountable. Indeed, there is a case to be made that, in the pursuit of making your Fluent API work well for Unit Testing, you are forced to decouple your code, which is a good thing.
But, there is undoubtedly more to think about.
Glorified constructors: an example of Fluent API overkill
There is a common overuse of Fluent APIs: using them to create objects where a simple constructor would be perfectly adequate.
Consider the following class:
We can then create a new car, like so:
Is there any reason to create a Fluent API instead? With a Fluent API, we could do this:
In the majority of cases, the answer will be: No!
One could argue that the Fluent API version shows more clearly what each parameter does. And one would be correct.
But, we can easily give the same clarity to a standard class constructor by changing its signature to expect a configuration object instead:
And, then we can build our car like this:
Now, what is expected for this Car class is absolutely clear. And the typing system (TypeScript in this case, but the same would apply for C# or Java) will prevent us from trying to build a car with part of the configuration missing:
As can be seen in the Car
example above, passing a configuration object
to a constructor is particularly easy in TypeScript, as it allows very
straightforward, inline initialisation of objects.
With Java or C#, things are a little more complex; you would
likely need to create a dedicated CarConfig
class, and have that passed
to the Car
constructor.
Despite all this, there is still a case to be made that the Fluent API version is clearer and easier to use. And that gets to the heart of this:
If you think using a Fluent API will make life easier for your consumers, for example because they are not familiar with class constructors as above, then use one.
But bear in mind that this will come with a maintenance cost; you do not get it for free, even when using Fluent API Generator to help you.