Thursday, 22 January 2009

A Pattern for Fluent Syntax

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 Interface

Typically 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 Works

The 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 it

Paradoxically, 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;
}
}

3 comments:

Bernie said...

May I suggest using Path.Combine()? Your caller can then leave leading or trailing backslashes on the folder name.

I *think* this allows for instantiation with a drive and path does it not? This would allow the FolderLexicon constructor to accept a full path. I think this is important for being able to use these classes when opening system folders such as %windir% that include the drive and path.

I like the concept very much, and I have been trying for a while now to make my code "speak the meaning". I think debugging and maintenance is often ignored and as a result few people want to dive into existing code bases. If your approach is adopted more widely, we may one day free ourselves of that dread.

Richard Nagle said...

I think Path.Combine() is an excellent suggestion. If nothing else, it alerts me to the existence of the method. (As an aside, isn't it time .Net shipped with a better API for dealing with the file system? The current system where everthing is dealt with as strings is IMHO woeful)

My example wasn't meant as a fully functioning API, and I'm sure there are many improvements. It was really only there to show how to string lexicons together.

I'm glad you liked the blog and hope it will of some use.

R

bhofmann said...

I went so off topic I blogged about it instead:

http://bernhardhofmann.blogspot.com/2009/01/file-systems.html