I've always been interested in making code as readable as possible and recently have become increasingly interested in fluent APIs. I have noticed that most of my fluent work has followed a similar pattern. I present this pattern here in the hope it helps others deliver their own fluent interfaces.
Fluent InterfaceTypically APIs provide methods with multiple parameters, many of which are not required for most uses. To overcome this, many programming languages have features such as method overloading or optional parameters. Whilst in many cases this helps API usage, it can result in bloated and hard-to-use APIs.
Fluent APIs take a different approach, breaking down the API into its consituent parts and guiding the programmer through its usage. Fluent API takes advantage of the features of modern development environments, particularly "code-complete", to help provide this guidance. The aim for a fluent API is that it should be easy to write and easy to read.How it WorksThe fluent syntax of your API will be provided by the public methods of a class; this class is named the
Lexicon. The key to the pattern, is that each method called should build up some state in the Lexicon and then return a reference back to a lexicon so that another method can be used from a Lexicon, and so on, to build up a meaningful statement.
Consider the following fluent syntax for counting files in a folder:
int files = FileSystem.GetDrive("C:")
.OpenFolder("Program File")
.GetFileCount();
FileSystem.GetDrive is the
entry-point into this fluent API. The entry-point will often be a constructor or static creation method. In all cases it will return a reference to a lexicon instance.
OpenFolder is a
continuation. Continuations are methods that will set some state within the lexicon and then return an instance to the lexicon.
GetFileCount is an
end-point for this API. An end-point is a method that takes some action based on the state of the lexicon. It will return some value that is the result of the fluent API call, or void, but will not return an instance of a lexicon.
A lexicon can have multiple entry-point, continuations, and end-points. Where the fluent API is large, it is common to break the API into more than one lexicon. Control is passed from one lexicon to another by a continuation which returns an instance of a different lexicon type. The new lexicon will probably need to know the context of the original lexicon; this is normally done by passing the original lexicon to the new lexicon in its constructor.
When to use itParadoxically, whilst a fluent API makes the reading and writing of that API easier, often the code making up that API is complicated by the addition of the fluent syntax. Consequently, Fluent Interface, should only be used in areas where the effort of extending the syntax is rewarded by frequent reuse. Typically, this applies to areas of cross-cutting concern such as configuration, logging or auditing.
It is worth considering the complexity of the API before implementing it as fluent. For a simple interface with only a handful of methods it is probably not worth the effort.
Example (C#)For this example we are going to consider a fluent syntax for obtaining information about the file system. I have split the API into multiple lexicons. This would possibly be considered overkill in the real-world but have done so here to make the example more complete.
Firstly lets look at the FileSystem lexicon which provides fluent syntax for dealing with drives on your system:
public class FileSystem
{
internal string DriveSpec { get; private set; }
private FileSystem(string driveSpec)
{
DriveSpec = driveSpec;
}
// 1st entry point
public static FileSystem GetDrive(string driveSpec)
{
if(!IsValidDrive(driveSpec))
{
throw new ArgumentException("Not a valid drive");
}
return new FileSystem(driveSpec);
}
// 2nd entry point
public static FileSystem MapDrive(string location, char assignedLetter)
{
string driveSpec = assignedLetter.ToString();
if(IsValidDrive(driveSpec))
{
throw new ArgumentException("Drive is already assigned");
}
NetworkUtilities.MapDrive(location, assignedLetter);
return GetDrive(driveSpec);
}
// continuation transfering control to another lexicon
public FolderLexicon RootDirectory()
{
return new FolderLexicon(this,"\\");
}
// 1st end point
public long AvailableBytes
{
get
{
DriveInfo info = new DriveInfo(DriveSpec);
return info.AvailableFreeSpace;
}
}
// 2nd end point
public string Name
{
get
{
DriveInfo info = new DriveInfo(DriveSpec);
return info.VolumeLabel;
}
}
}
And now the FolderLexicon used to hold the syntax for accessing information about file system folders
public class FolderLexicon
{
// holds a reference to FileSystem
// so the entire context can be discovered
private readonly FileSystem drive;
private readonly StringBuilder path = new StringBuilder();
// entry point - note that this lexicon can
// only be entered through a FileSystem
internal FolderLexicon(FileSystem drive)
{
this.drive = drive;
path.Append(drive.driveSpec);
path.Append("\\");
}
//continuation
public FolderLexicon OpenFolder(string dirSpec)
{
path.Append("\\");
path.Append(dirSpec);
if(!Directory.Exists(path.ToString()))
{
throw new ArgumentException("Folder does not exist");
}
return this;
}
//end point
public int GetFileCount()
{
DirectoryInfo dInfo = new DirectoryInfo(path.ToString());
return dInfo.GetFiles().Length;
}
}