Eric Evans and Martin Fowler worked together on refining the idea of a
Fluent Interface http://www.martinfowler.com/bliki/FluentInterface.html
. Most of what we'll be covering in this article is attempting to build a
Fluent Interface through Method Chaining http://martinfowler.com/dslwip/MethodChaining.html
. In my last article, I discussed developing code by first writing out a
sketch of how the code should be consumed first. Next we
would build out the surface are and unit tests. Next we would write the
unit tests to define the functionality we want. Finally we implement the
classes and get our unit tests to pass.
The two goals we have in building a fluent interface pattern is that we want
code to be readable and we want the code to flow. One of the tricky parts
is to keep the primary objects understandable by themselves. We need to
be careful to keep classes as cohesive as possible and not build a cluttered object model. This is why it is
good to work towards separation between the "fluent" objects and our primary
domain objects.
Here is a sketch of some code that I would like to build an object model to "fit"
around.
Egg humptyDumpty = new
Egg("Humpty Dumpty");
Wall wall = new Wall("Great wall of China");
humptyDumpty
.Does
.SitOn(wall)
.HasFall(Severity.Great);
|
One of the things we want to keep in mind as we go is to not clutter the
"Egg" object with unnecessary methods. As a matter of fact, we'll be
keeping the "Egg" object pretty clean where it just retains some state and use
our Fluent Interface object to hold the methods that change the state of the
Egg. This way, the Egg class stays relatively clean and understandable by
itself (nobody likes dirty eggs).
We'll take our sketch and put it in a temporary class that we'll use for
building out the API.
public class
Main
{
public void Run()
{
Egg humptyDumpty = new Egg("Humpty Dumpty");
Wall wall = new Wall("Great
wall of China");
humptyDumpty
.Does
.SitOn(wall)
.HasFall(Severity.Great);
}
}
|
Now I'll be using the Refactor VStudio plug-in from DevExpress to build the
classes since this functionality isn't built into VStudio (yet). Using
this tool, I can right click on the not-yet-implemented class and I get an option
to declare the class.
I'll also declare the Wall class while I'm at it.
public class
Egg
{
internal Egg(string
param1)
{
}
}
public class Wall
{
internal Wall(string
param1)
{
}
}
|
Now we can start building out the properties and methods for each
class. This is where the Method Chaining pattern will come in handy to
help us build our Fluent Interface. The basic idea of the Method Chaining
pattern is to be able to chain methods together in a way that they make sense
linguistically. The methods will pass an object along to retain
state. In this case it will be our Egg object.
An example of this method chaining is in our sketch:
humptyDumpty
.Does
.SitOn(wall)
.HasFall(Severity.Great);
|
We'll be using this pattern when we construct the objects needed for
exposing the methods. The first thing we need is an object that serves to
perform different actions on an Egg object.
And we change the signature a bit to expose a yet undefined class called
EggActionPart.
public class
Egg
{
internal Egg(string
param1)
{
}
public EggActionPart Does
{
get
{
throw new NotImplementedException();
}
}
}
|
When we generate the EggActionPart class we need to be change the
constructor so that we keep a reference to the Egg object in order to change its
state from the EggActionPart.
public class
EggActionPart
{
internal EggActionPart(Egg
target)
{
}
}
|
And change the return type of the method returns the same EggActionPart so
that we can use the class to chain method calls.
public class
EggActionPart
{
internal EggActionPart(Egg
target)
{
}
public EggActionPart
SitOn(Wall wall)
{
throw new NotImplementedException();
// TODO: do stuff to egg here
return this;
}
}
|
So now when we generate the "HasFall()" method, it will be generated on the
EggActionPart class.
And we'll again modify the generated method signature so we can do more to
the Egg as our EggActionPart class grows.
public class
EggActionPart
{
internal EggActionPart(Egg
target)
{
}
public EggActionPart
SitOn(Wall wall)
{
throw new NotImplementedException();
// TODO: do stuff to egg here
return this;
}
public EggActionPart
HasFall(Severity severity)
{
throw new NotImplementedException();
// TODO: do stuff to egg here
return this;
}
}
|
The EggActionPart class is a concise example of using Method Chaining to
build a Fluid Interface and hopefully you can see how easy it is to generate from the outside in.
This pattern is also much easier to understand while developing our API from the
outside-in. Getting this pattern to work from the inside-out is much more
difficult and more error prone. Notice how we have kept a relatively
clean separation between the Egg class which can now just retain state and the
Fluent Interface object which provides us with Method Chaining.
Next we'll look at setting and retaining state in the Egg object from the
EggActionPart object. First we need a member variable in the
EggActionPart object to keep track of the Egg being acted on.
public class
EggActionPart
{
internal EggActionPart(Egg
target)
{
m_Target = target;
}
private readonly Egg
m_Target;
public EggActionPart
SitOn(Wall wall)
{
throw new NotImplementedException();
// TODO: do stuff to egg here
return this;
}
public EggActionPart
HasFall(Severity severity)
{
throw new NotImplementedException();
// TODO: do stuff to egg here
return this;
}
}
|
Next we will have to tackle the "SitOn()" method. We have to figure
out what makes the most sense in terms of our domain. Is it important for
the Egg to keep track of what it was sitting on? Does the wall need to
know what is sitting on it? Is it important to keep track of the Egg's
posture (sitting, standing, lying down, etc)? Since this is a fake domain
we can take the liberty of just picking something, but in a real project unless
it is perfectly clear, this is the point where we would probably want to talk
with a domain expert and figure out what the correct modeling would be.
Now we'll start to walk down this path just a short ways so you can see how
coding from the outside-in will help us determine the functional requirement of
our classes before too much has been implemented.
We will say that the Wall object needs to keep track of what is on it and
the Egg needs a method to enable it to sit.
public class
EggActionPart
{
internal EggActionPart(Egg
target)
{
m_Target = target;
}
private readonly Egg
m_Target;
public EggActionPart
SitOn(Wall wall)
{
throw new NotImplementedException();
m_Target.SetState(EggState.Sitting);
wall.AddSitter(m_Target);
return this;
}
public EggActionPart
HasFall(Severity severity)
{
throw new NotImplementedException();
// TODO: do stuff to egg here
return this;
}
}
|
Now we need to create the EggState enum. We know that Humpty-Dumpty will
most likely fall off the wall and be broken, so we'll add a state for that
while we're here.
public enum
EggState
{
Sitting,
Broken
}
|
Next we will look at stubbing out the Egg.SetState() and Wall.AddSitter()
methods.
If the Egg falls, it is no longer sitting on the wall and we need a way to
remove it. Now the complexity of our domain is starting to show its face
and we'll have to change our previous choice of how to implement the
EggActionPart.SitOn() method. We need to better handle this relation
between the Egg and the Wall. There are potentially other things that the
Egg can sit on and other things that can sit on the Wall and we should probably
allow for this so we'll generalize this relationship using two
interfaces. This would be an expensive change if there were lots of
implementation code in place, but because we are coding from the outside-in,
this will have minimal impact.
public interface
ISitter
{
Boolean IsSitting { get;
}
ISitable Seat { get;
set; }
}
public interface
ISitable
{
void AddSitter(ISitter
sitter);
void RemoveSitter(ISitter
sitter);
ISitter[] GetSitters();
}
|
Now we'll make the Egg class implement the ISitter interface and stub out
the methods by right clicking and selecting "Implement Interface".
public class
Egg: ISitter
{
internal Egg(string
param1)
{
}
public void SetState(EggState eggState)
{
throw new NotImplementedException();
}
public EggActionPart
Does
{
get
{
throw new NotImplementedException();
}
}
#region
ISitter Members
public bool
IsSitting
{
get { throw new NotImplementedException();
}
}
public ISitable
Seat
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
#endregion
}
|
We'll do make the Wall implement ISittable in the same way:
public class
Wall:ISitable
{
internal Wall(string
param1)
{
}
public void
AddSitter(Egg target)
{
throw new NotImplementedException();
}
#region
ISitable Members
public void
AddSitter(ISitter sitter)
{
throw new NotImplementedException();
}
public void
RemoveSitter(ISitter sitter)
{
throw new NotImplementedException();
}
public ISitter[]
GetSitters()
{
throw new NotImplementedException();
}
#endregion
}
|
Now we can back up and re-structure the EggActionPart.HasFall() method.
Notice that I leave the NotImplementedException in place. I do this so that as
I build out the unit tests I am keeping track of the methods I have
implemented. This placeholder helps keep us in the TDD mindset for when we go
to write tests and implementations.
public class
EggActionPart
{
internal EggActionPart(Egg
target)
{
m_Target = target;
}
private readonly Egg
m_Target;
public EggActionPart
SitOn(ISitable item)
{
throw new NotImplementedException();
item.AddSitter(m_Target);
return this;
}
public EggActionPart
HasFall(Severity severity)
{
throw new NotImplementedException();
m_Target.Seat = null; // there might be a better way to handle this
m_Target.SetState(EggState.Broken);
return this;
}
}
|
This is where it is important to start putting unit tests in place to ensure
that the states of the Egg object and it's relation to the Wall object are
handled correctly. As you use this approach to coding you will quickly
find the places in the domain that are hiding complexity and this is the
opportunity to pin it down with unit tests to ensure the final code works as
expected. I usually write more tests as I go through this process because
sometimes they help me figure out if I am opening Pandora's Box by
going down a certain path. Even though it is sometimes painful, I never
feel bad about going back and restructuring some of the calls amd my sketch as I go because
it is far less expensive to do it now rather than when the code is done (so I'm
actually quite happy because my model will be that much better). If I can
find a simpler way to express something with the way I am structuring the
classes it is always worth changing. If you do the same, you'll thank
yourself a few iterations down the road.
Hopefully you can see how once we start digging in a bit something seemingly
simple it can grow in complexity rather quickly and how if we code from the
outside-in these changes can be handled early in the coding cycle rather than
after there is a bunch of implementation code in place and time has been wasted on
writing code that has to be thrown out or heavily refactored. In my next
article, we'll look at building out a bit more complex surface are that will
take some more thought and a few more tricks.
Until then,
Happy coding