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/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube