Monday, December 8, 2008

On Interface Design with Extension Methods

Extension methods that Microsoft added to C# 3.0 initially look like a syntax sugar. Initially, the idea appears very simple: Microsoft needed a way to add functionality to classes and interfaces that they cannot change; extension methods appear to be a quick and dirty way they dealt with the problem.

However, extension methods have a less intuitive use: they let designers provide implementations "horizontally," as opposed to providing it "vertically" through the chains of inheritance. Other languages such as C++ and Java do not have a comparable mechanism. In C++ you would end up placing functionality in a base class, and inheriting it virtually; in Java you would make the interface very large (see ResultSet for an example) because you cannot put implementations into an interface.

Consider an example. Let's say that we are modeling an interface for accessing a row of data that we read from a database (similar to Java's ResultSet interface). Here is how the interface could look:

public interface IFieldSchema {
    string Name { get; }
    int Index { get; }
}

public interface IRowSchema {
    int FieldCount { get; }
    IEnumerable Fields { get; }
    IFieldSchema GetField(string name);
}

public interface IRow {
    IRowSchema Schema { get; }
    T GetField<T>(int index);
    void SetField<T>(int index, T value);
    T GetField<T>(string name);
    void SetField<T>(string name, T value);
}

Take a look at the last two methods of IRow: on one hand, they seem unnecessary, because you can do the same thing by first obtaining an index from the schema, and then calling the corresponding GetField/SetField method; on the other hand, this will cause inconvenience to the users of the interface who prefer using names instead. Convenience drives the addition of both methods to the interface - this is precisely what designers of Java's ResultSet did. In doing so, however, the developer of the interface pays for the convenience of developers using the interface with the effort of the developers who implement his interface. What's even worse than that is that the method will have the same code in all implementations.

At this point, the designer of the interface may either take an abstract class route, or build a class that provides the needed implementation statically. Here is how the above example would look with an abstract class:

public abstract class AbstractRow : IRow {
    public IRowSchema Schema { get; private set; }
    public abstract T GetField<T>(int index);
    public abstract void SetField<T>(int index, T value);
    protected AbstractRow(IRowSchema schema) {
        Schema = schema;
    }
    public virtual T GetField<T>(string name) {
        return GetField<T>(Schema.GetField(name).Index);
    }
    public virtual void SetField<T>(string name, T value) {
        SetField(Schema.GetField(name).Index, value);
    }
}

This is better, because developers of classes inheriting IRow no longer need to code the "extra" methods. Unfortunately, this will force them to inherit AbstractRow, which may not be possible, considering the single-inheritance scheme of C#.

Extension methods provide an elegant way of solving this problem by moving the code of the "extra"methods into an extension method. Inheriting IRow becomes much easier, while nearly all functionality remains in place.

public interface IRow {
    IRowSchema Schema { get; }
    T GetField<T>(int index);
    void SetField<T>(int index, T value);
    T GetField<T>(string name);
    void SetField<T>(string name, T value);
}

public static class RowExtension {
    public static T GetField<T>(
        this IRow row, string name
    ) {

        return row.GetField<T>(
            row.Schema.GetField(name).Index
        );

    }
    public static void SetField<T>(
        this IRow row, string name, T value
    ) {

        row.SetField(
            row.Schema.GetField(name).Index, value
        );

    }
}

The reason I say "almost" is that implementations of IRow can no longer override the code for accessing fields by name. In neraly all cases, however, it's a good riddance - consider, for example, building a new class implementing an interface that is similar to java.sql.ResultSet, with half of its methods already coded for you!