Internal DSLs, method chaining and discoverability

Paul says “The whole .NET space has gone fluent interface crazy”, and he is quite right. Everybody has their own fluent interfaces, and unless I’m missing the bigger picture, most of them seem to be about productivity over discoverability. The intent is clear, write more intent-revealing code in less time.

More often than not however, you multiply the entry points needed to be known by developers. Let’s take an example with the criteria API in nHibernate.

return Session.CreateCriteria(typeof(FundingCategory), "fc")
   
.CreateCriteria("FundingPrograms", "fp")
   
.CreateCriteria("Projects", "p", JoinType.LeftOuterJoin)
   
.Add(Restrictions.Disjunction()
       
.Add(Restrictions.Eq("fp.Recipient.Id", recipientId))
       
.Add(Restrictions.Eq("p.Recipient.Id", recipientId))
   
)
   
.SetProjection(Projections.ProjectionList()
       
.Add(Projections.GroupProperty("fc.Name"), "fcn")
       
.Add(Projections.Sum("fp.ObligatedAmount"), "fpo")
       
.Add(Projections.Sum("p.ObligatedAmount"), "po")
   
)
   
.AddOrder(Order.Desc("fpo"))
   
.AddOrder(Order.Desc("po"))
   
.AddOrder(Order.Asc("fcn"))
   
.List<object[]>();
[nitpicker corner: no one ever said the criteria api was a fluent api, it’s at most method chaining.]

I’ve highlighted in red those entry points. Each of those method usually takes an instance of an interface. A static method is then used to create those objects. What’s the issue with that?

It all comes down to discoverability. One of the biggest issue I’ve always had with the criteria API is the low discoverability of those types. Take the ICriterion interface, for which the Expression class provides the simple criterions. The only way, once in a method, for me to know that where it expects ICriterion I could use Expression is by going to the documentation. This is for me a massive discoverability failure.

The second issue comes from the latest lambda-based APIs. Let’s have a bit of code from the original blog post from Dru that Paul was referring to.

  1.  static TestDeployment()  
  2.     {  
  3.         Define(() =>  
  4.             {  
  5.                 During(Web, (p) =>  
  6.                     {  
  7.                         p.OnServer("WebServer")  
  8.                             .IisSite("Apps")  
  9.                             .VirtualDirectory("dashboard")  
  10.                             .Verify()  
  11.                             .CreateIfItDoesntExist();  

I’ve not downloaded the code to have a play, but I assume that Define and During are actually properties on the base type. On a positive note, this does help with discoverability, at the cost of enforcing an inheritance hierarchy.

So why do I feel awkward with this syntax? The nesting of lambdas. The multiplication of p, x and other one-letter prefix make the code much less readable. The pen*s operator is not exactly my favourite addition to the C# compiler.

Here’s my checklist for what constitutes a nice usable fluent API. [nitpicker corner: OpenRasta doesn’t do it that way everywhere. You didn’t say the same thing six months ago].

  • No lambda-based chaining
  • No multi-line lambda expressions
  • At most one or two static class entry-points to discover
  • Method parameters should only be primitive types

There is a way to make a fluent API while respecting those points, but I believe the result would be better. So here’s Dru’s example rewritten using those guidelines.

using(Definition)
{
    During.Web
        .Server("WebServer")
            .Has.IisSite("Apps")
                .VirtualDirectory("dashboard")
                .Verify()
                .CreateIfItDoesntExist()
        .And
        .Server("SvrTopeka19")
            .Has.Msmsq()
                .PrivateQueueNamed("mt_subscriptions")
                .CreateIfItDoesntExist()
        .And
        .CopyFrom(@".\code_drop\dashboard\**\*.*").To(@"\\webserver\apps\dashboard\");
}

The main difference here is the introduction of scoping operators. The Server method returns two properties, And which closes the current Server scope, and Had Has which open a new service-specific scope.

I find this kind of API much more readable, and they introduce less syntax noise (aka no lambda operators or blocks). There is also no need to introduce external points of references, be them be properties or static classes, except for the original entry point. Arguably, they’re harder to write, but I think the effort is worth the benefits.

Ads

Comment