BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Programming Concepts

Fluent Interfaces

Sometimes the public interfaces of object-oriented libraries lead to complex code that can be difficult to quickly understand. Fluent interfaces change the way that classes are written in order to help developers create code that mirrors natural language.

What is a Fluent Interface?

When developing software, it is important that the code that you write is easy to understand in order that it can be maintained at a later date. There are many ways in which you can structure your classes in order to achieve this and various guidelines, such as the five SOLID principles. One way to achieve improved readability for your source code is to develop libraries with fluent interfaces.

Fluent interfaces allow code to be created that appears similar to natural language, when the symbols that decorate methods are ignored. This involves choosing member names that can be combined in ways that mirror words in a language such as English. The words are then combined using method chaining, where each method returns a result that can be acted upon by the next call in the sequence. However, fluent interfaces are not designed for method chaining alone.

To understand the difference between a fluent and non-fluent interface, let's look at an example. The code below uses method chaining but if the member names are read out loud, the meaning is not perfectly clear. The code calculates the date and time one week from the current date at 9:30am.

DateTime time = DateTime.Now.AddDays(7).Date.AddHours(9).AddMinutes(30);

This simple example is reasonably easy to understand for a developer but as the complexity grows it could become more difficult. If we convert the code to use a more fluent syntax, we can make its purpose clearer. The code below, read aloud, is "One week hence at 9:30".

DateTime time = 1.Week().Hence().At(9, 30);

Fluent interfaces are used widely in frameworks and library code. Two common examples that have been examined in earlier articles are Language-Integrated Query (LINQ) and Moq. You can create your own fluent interfaces by adding suitably named methods to your classes. You can also take advantage of C# 3.0's extension methods to add fluent-style methods to existing classes, including .NET framework types. We'll use this technique in the following sections to create a small fluent interface for date and time processing that supports the code example above.

Integer Extensions

In the fluent example code above, the first method used is "Week". This is an extension method of the integer type that creates a TimeSpan value representing the specified number of weeks. The code below shows how the method is created, as well as several other integer extension methods that create TimeSpans for days, hours and minutes.

public static class FluentTimeExtensions
{
    public static TimeSpan Days(this int number)
    {
        return new TimeSpan(number, 0, 0, 0);
    }

    public static TimeSpan Hours(this int number)
    {
        return new TimeSpan(number, 0, 0);
    }

    public static TimeSpan Minutes(this int number)
    {
        return new TimeSpan(0, number, 0);
    }

    public static TimeSpan Weeks(this int number)
    {
        return new TimeSpan(7 * number, 0, 0, 0);
    }
}

You might have noticed that the name of each of the above methods is plural whilst the initial example used the singular "Week". When you are creating a fluent interface for your libraries you may decide to implement both singular and plural names. The small overhead of the additional code is often outweighed by the increased readability.

public static TimeSpan Day(this int number)
{
    return Days(number);
}

public static TimeSpan Hour(this int number)
{
    return Hours(number);
}

public static TimeSpan Minute(this int number)
{
    return Minutes(number);
}

public static TimeSpan Week(this int number)
{
    return Weeks(number);
}

With these extension methods in place we can use expressions such as the following:

1.Week()
3.Hours()

TimeSpan Extensions

The second method in the initial example is an extension method of the TimeSpan type. In the sample code the Hence method adds the TimeSpan to the current time. This method is shown below, along with four others. One finds a date and time that is a specified amount of time in the past. The other two add or subtract time from a date and time supplied to a parameter. The last method, "And", combines two TimeSpans.

public static DateTime Hence(this TimeSpan time)
{
    return DateTime.Now.Add(time);
}

public static DateTime Ago(this TimeSpan time)
{
    return DateTime.Now.Add(-time);
}

public static DateTime After(this TimeSpan time, DateTime startingTime)
{
    return startingTime.Add(time);
}

public static DateTime Before(this TimeSpan time, DateTime startingTime)
{
    return startingTime.Add(-time);
}

public static TimeSpan And(this TimeSpan current, TimeSpan additionalTime)
{
    return current.Add(additionalTime);
}

Combining the integer and TimeSpan extension methods allows expressions like the following:

3.Weeks().Ago()
3.Weeks().And(2.Days()).After(new DateTime(2012, 5, 12))

DateTime Extensions

The final method in the first fluent interface example is an extension method of the DateTime type. The "At" method changes the time part of the DateTime to a specified value, whilst the date section is unaffected:

public static DateTime At(this DateTime dateTime, int hour, int minute)
{
    return dateTime.Date + new TimeSpan(hour, minute, 0);
}

Adding the final method allows the following expressions:

1.Week().Hence().At(9, 30)
2.Days().After(new DateTime(2012, 5, 12)).At(16, 30)

Considerations

Fluent interfaces can be used to improve the readability of code. However, you should take care when creating classes with such interfaces, especially if they are likely to be used in a non-fluent manner. The names of methods in a fluent interface are often not particularly descriptive and can be poor in a non-fluent context. You should consider whether or not a fluent interface is suitable for all use cases. If it is not, you may wish to implement a non-fluent interface in addition to, or instead of, the fluent version.

12 May 2012