Practical .NET
Implementing a Fluent Interface
Here's how to implement a fluent API for a single class that supports the goals of fluent interfaces.
In this column, I'm going to demonstrate how to implement a fluent interface in a single class. The initial version of this interface supports typical actions on a SalesOrder class that holds a collection of OrderLine objects:
Public Class SalesOrder
Public Property OrderLines As New List(Of OrderLine)
End Class
Public Class OrderLine
Public Property Quantity As Integer = 0
Public Property ProductId As String = String.Empty
End Class
I described the interface I'm going to implement in more detail in an earlier column, but if you're only interested in the "how" rather than the "why," you don't need to read that article. Here, I'll just be implementing two of the methods described in that earlier column.
A Naive Implementation
The easiest way to implement a fluent interface for a single class is simply to add the appropriate methods to the class. Listing 1, for example, shows the SalesOrder class extended with Add and Remove methods that update the SalesOrder object's OrderLines collection and that can be used in a fluent way.
Listing 1: A SalesOrder Class with Fluent Methods
Public Class SalesOrder
Public Property OrderLines As New List(Of OrderLine)
Public Property DeliveryDate As Date = Date.Now
Public Property TotalValue As Decimal
Public Function Add(productId As String,
quantity As Integer) As SalesOrder
If Me.OrderLines.Where(
Function(fi) fi.ProductId = productId).Count = 0 Then
Dim ol As New OrderLine With {.ProductId = productId,
.Quantity = quantity,
.Order = Me}
Me.OrderLines.Add(ol)
End If
Return Me
End Function
Public Function Remove(productId As String) As SalesOrder
Me.OrderLines.RemoveAll(
Function(fi) fi.ProductId = productId)
Return Me
End Function
End Class
Here's code that would, effectively, replace one OrderLine with another by using the Remove and Add methods in a fluent way:
so.Remove("A123").
Add("A123", 4)
The key to making this a fluent interface is having each method return an object that the next method in the chain can be called from -- in this case, the SalesOrder object.
Isolating the Interface
However, this design intermixes the "fluent" part of the SalesOrder's interface with the typical, non-fluent members of the SalesOrder's interface. By "typical," I mean those methods or properties that either don't return anything or don't return a SalesOrder object. As a result, those "typical" methods either terminate the chain or force it to turn away from modifying the SalesOrder object. If a developer uses one of those non-fluent members of the SalesOrder's interface, then the developer won't be able to continue chaining methods together to update the SalesOrder as I did in the previous code example.
Since one of the reasons developers create fluent interfaces is to provide a more understandable interface, I'd prefer to isolate the fluent members of the SalesOrder's interface from the rest of the SalesOrder's members, to make it obvious which members are (and aren't) "fluent." The easiest way to do that is to move the fluent members into a nested class within the SalesOrder class. To retrieve the nested class and to manage its creation, I add a property to the SalesOrder class which returns the nested class (I've called that property "Manage"). That nested class will need a reference to the SalesOrder that it's working with, so I pass the SalesOrder to the nested class' constructor. The resulting SalesOrder class, with the details of the methods removed, looks like Listing 2.
Listing 2: A SalesOrder Class with an Isolated Fluent Interface
Public Class SalesOrder
Public ReadOnly Property Manage As ManageOrder
Get
Return New ManageOrder(Me)
End Get
End Property
Class ManageOrder
Private Order As SalesOrder
Public Sub New(order As SalesOrder)
Me.Order = order
End Sub
Public Function Add(
productId As String, quantity As Integer) As SalesOrder
Return Me.Order
End Function
Public Function Remove(
productId As String) As SalesOrder
Return Me.Order
End Function
End Class
End Class
A fluent call to remove an OrderLine now looks like this:
so.Manage.Remove("A123")
However, because my fluent methods are in my ManageOrder class and my methods return the SalesOrder object, to use this interface I would have to keep returning to the Manage method when building a chain of calls. The chain of calls that replaces an Orderline would look like this, for example:
so.Manage.Remove("A123").
Manage.
Add("A123", 4)
Simplifying the Interface
That's obviously not satisfactory. Fortunately, the solution is simple: Just have the methods in the ManageOrder class return the ManageOrder object rather than the SalesOrder object. The revised version of the nested class looks like Listing 3.
Listing 3: A More Fluent Nested Class
Class ManageOrder
Private Order As SalesOrder
Public Sub New(order As SalesOrder)
Me.Order = order
End Sub
Public Function Add(
productId As String, quantity As Integer) As ManageOrder
Return Me
End Function
Public Function Remove(
productId As String) As ManageOrder
Return Me
End Function
The fluent code to replace an OrderLine now looks like this:
so.Manage.Remove("A123").
Add("A123", 4)
Because this call includes the reference to the Manage property, there's a little more code in this fluent chain than in my original version. The benefit of this design appears when the developer types the period after typing in "Manage." Only the fluent methods appear in the IntelliSense list presented to the developer.
This is, however, a limited solution: The fluent interface is implemented for, and only works with, a single class. I'm going to return to this topic in a future column to show how this design can be extended to create a single implementation that supports several classes.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.