Introduction
Fluent interfaces are a fun and discoverable way to allow fellow
developers to access functionality. If you've every used Linq's dot(.)
notation, then you've used a fluent interface. Not only are fluent
interfaces easy to use, but with the right knowledge they are also easy
to create.
If you don't know what a fluent interface is, then let me show you
through a simple example. This example allows you to calculate the miles
per gallon for a vehicle given a few required values:
var mpg = new Car(make : "audi", model: "a4")
.WithHorsePower(250)
.WithFuel(gallons:10)
.WithWeight(tons:3)
.Mpg();
Compare the fluent approach with the traditional C# get/set pattern.
var car = new Car(make: "audi", model: "a4") {
HorsePower = 250,
Fuel = 10,
Weight = 3
}
var mpg = car.Mpg();
Both approaches to this particular problem are perfectly fine. So why would you use a fluent interface over the traditional method?
In my humble opinion, I find that (good) fluent interfaces help
dictate direction, and keep you from missing a crucial step in your
setup. In the traditional approach what would stop us from calling the Mpg
method before setting any of the other properties? The answer is
nothing, and that can leave your developer in a state of confusion as to
why their result is incorrect or throwing exceptions.
I also believe that fluent interfaces do a better job of
encapsulation. It would be very difficult for another developer or part
of your application to modify values accidentally. This would be
possible in the traditional approach, and not possible with the fluent
interface.
if (car.Fuel = 30) // doh!
So let's break down our example and see what parts are important to a fluent interface.
Entry Point
new Car(make : "audi", model:"a4") // entry point
Every fluent interface needs an entry point. A place where the
developer must start in order to begin the chaining process. In our
example, the entry point is just a simple constructor, but does not have
to be: It could be a static method, a method, or property.
Chaining Methods
.WithHorsePower(250)
.WithFuel(gallons:10)
.WithWeight(tons:3)
Once you get passed the entry point, you are left with all the
chaining methods. Chaining methods are capable of many things, but they
are usually allow you to set properties. Nothing mind blowing here, but
these methods are critical to setting the state you need for the
executor.
Executor(s)
.Mpg(); // the executor
The executor is arguably the most important part of the fluent
interface. It allows you to escape the chaining methods and finally
return a result. Think of the executor method as the period on the end
of a sentence. You are also not limited to one executor, you may have as
many as makes sense to your API.
Now that I've broken down the different parts of the fluent interface, let's build it!
Show Me The Code!
How do we get to the interface we have above? let's start by knowing we need a Car class.
public class Car
{
private readonly string _make;
private readonly string _model;
public Car(string make, string model)
{
_make = make;
_model = model;
}
}
So this helps us define the entry point. Our entry point in this case is the Car constructor. The next step is to build the chaining methods.
public class Car
{
private readonly string _make;
private readonly string _model;
private int _bhp;
private int _gallons;
private int _tons;
public Car(string make, string model)
{
_make = make;
_model = model;
}
public Car WithHorsePower(int bhp)
{
_bhp = bhp;
return this;
}
public Car WithFuel(int gallons)
{
_gallons = gallons;
return this;
}
public Car WithWeight(int tons)
{
_tons = tons;
return this;
}
}
Here is the real magic. I am returning the Car
itself after every method call. This allows you to chain to the next
method. In this example, I am not constraining what methods you might
want to call next, or the fact that you have to call any of them. If you
want to constrain what the next call is, you can return another class /
interface with one method (the next method). Note, calling the same
method multiple times will just override the old value.
Finally, let's implement the executor. I've also taken the liberty to
allow developers access to the values through readonly properties.
public class Car
{
public int Gallons { get; private set; }
public int Tons { get; private set; }
public int Bhp { get; private set; }
public string Make { get; private set; }
public string Model { get; private set; }
public Car(string make, string model)
{
Make = make;
Model = model;
}
public Car WithHorsePower(int bhp)
{
Bhp = bhp;
return this;
}
public Car WithFuel(int gallons)
{
Gallons = gallons;
return this;
}
public Car WithWeight(int tons)
{
Tons = tons;
return this;
}
public int Mpg()
{
// please don't use this for real mpg
return Gallons/Math.Max(Tons, 1);
}
}
It is important that the executor return something other than Car
or else you will still be stuck in the chaining methods. Also note that
readonly properties are also executors; calling any of them will not
allow you to chain any more methods. This class is now fluent and ready
to be used with the example from the introduction.
Conclusion
This pattern is very powerful and can be expanded once you realize
what you need to do to facilitate it. It doesn't work in every situation
but in those times where you would like to give guidance to other
developers this approach gives you a great way to bake that guidance
directly into your API.
Hope this helps you in better understanding the fluent interface
approach and if you have any questions or if I could make anything
clearer please don't hesistate to ask.
Links & Resources (Extra Credit)
Some fluent interface libraries that I use frequently. Looking at the code their can help you see advanced Fluent Interfaces.
Comments
Post a Comment