Wednesday, April 05, 2017

Bulletproof fluent interfaces

I attended a Fluent Interface webinar by the inimitable Scott Lilly (see http://scottlilly.com/FIWebinar). Below is my exercise file and some thoughts around what I think are the benefits of a fluent interface.

A fluent interface gives you more control over the way your methods are called. Scott explains it as a three step process, first instantiate, then chain your methods, and finally execute (ICE). And I will present an example of a report in the classic sense, and one built in the fluent way.

download linqpad file: http://share.linqpad.net/w5gdub.linq




First, here's a classic example:
void Main()
{
 Console.WriteLine(ClassicReportGenerator
  .BuildReport(
   //arg names optional - adding to contrast with fluid interface
   to:DateTime.Now.AddDays(5), 
   from:DateTime.Now,
   sortAscending: true
  ));;
}

class ClassicReportGenerator
{

 public static string BuildReport(DateTime to, DateTime from, bool sortAscending)
 {
  return string.Format("ClassicReportGenerator build report succeeded {0} - {1}. Sort {2}",
   from, to, sortAscending? " ascending" : "descending");
 }

The classic way of calling the build report method is very concise. Note that the incoming parameter list can be turse and you have to inspect what the true parameter is. You could get around that by annotating the field name (like this sortAscending:true) in the calling method. But let see what the fluid interface looks like.

What does it bring to the table? If you have many more options and you want them to be readable and be self-documenting, fluent interfaces is a way to do that. The complexity is an investment and the ROI is over time as it gets used more and more. You don't need to constantly overload your methods and it's easier for other developers to use your API as you intended.

As you use the fluent interface, you'll note that after instantiating the object, you can only call the From method on it, followed by the To method. Then you can optionally call the Sort method followed by Build or just Build without sorting. Intellisense is your friend. Here's the code:

void Main()
{
 Console.WriteLine(
  FluentReportGenerator.CreateReport()
  .From(DateTime.Now)
  .To(DateTime.Now.AddDays(5))
  .SortDescending()
  .Build()
 );
}

class FluentReportGenerator:ICanSetFromDate, ICanSetToDate, ICanBuildReport, ICanSortOrBuildReport
{
 DateTime _from { get; set; }
 DateTime _to { get; set; }
 bool _sortAscending { get; set;}
 
 private FluentReportGenerator() {}
 
 public static ICanSetFromDate CreateReport() {
  return new FluentReportGenerator();
 }

 public ICanSetToDate From(DateTime date)
 {
  _from = date;
  return this;
 }
 
 public ICanSortOrBuildReport To (DateTime date)
 {
  _to = date;
  return this;
 }

 public ICanBuildReport SortAscending()
 {
  _sortAscending = true;
  return this;
 }
 public ICanBuildReport SortDescending()
 {
  _sortAscending = false;
  return this;
 }

 public string Build()
 {
  return string.Format("FluentReportGenerator build report succeeded {0} - {1}. Sort {2}", 
   _from, _to, _sortAscending?" ascending":"descending");
 } 
}

#region Intefaces
 interface ICanSetFromDate { 
  ICanSetToDate From(DateTime date);
 }
 
 interface ICanSetToDate
 {
  ICanSortOrBuildReport To(DateTime date);
 }
 
 interface ICanSortOrBuildReport
 {
  
  ICanBuildReport SortAscending();
  ICanBuildReport SortDescending();
  string Build();
 }
 
 interface ICanBuildReport
 { 
  string Build();
 }
#endregion





No comments: