Interface anti-patterns: overload overload

Spread the love

Continuing on the theme of interface anti-patterns, let’s talk about overloaded methods. Specifically, let’s talk about overloaded method overload.

You’ve probably seen it before. You need to implement an interface and find that there are multiple overloads of each method. Overloading is not a problem per se, but fulfilling an interface like this requires a bunch of boilerplate code. As a concrete example, consider the classic WCF interface ICommunicationObject. For almost every method, it defines two copies: one with a timeout and one without. Implementation-wise, you are very unlikely to want a unique code path for each one, and in practice your code will probably look like this:

class MyCommObj : ICommunicationObject
{
    // . . .

    public void Open() => this.Open(DefaultOpenTimeout);

    public void Open(TimeSpan timeout)
    {
        // . . . core implementation here . . .
    }

    public void Close() => this.Close(DefaultOpenTimeout);

    public void Close(TimeSpan timeout)
    {
        // . . . core implementation here . . .
    }

    // . . . etc.
}

The code verbosity here is one drawback, but this situation also presents challenges for testing and maintenance. Extra methods mean extra tests, and these extra tests have to exist for every interface implementation if you do your due diligence and verify the contract. On the topic of maintenance, go check the code sample above again. The Close() method, despite being a single line, has a bug! This type of copy/paste coding is extremely common when such boilerplate is involved and presents more opportunities for error.

Pitfalls notwithstanding, method overloads are often helpful — to the caller. How can we retain this ease of use without sacrificing ease of implementation?

Here is one technique I have used with some success: instead of defining overloads directly in the interface, use extension methods. First, on the interface, define only the “most derived” copy of the method:

public interface IWidget
{
    Task RunAsync(Options options, CancellationToken token);
}

Then define extension methods (ideally in the same namespace as the interface) for all the combinations of parameters that you need:

public static class WidgetExtensions
{
    private static Options DefaultOptions => new Options() { /* defaults */ };

    public static Task RunAsync(this IWidget obj)
    {
        return obj.RunAsync(DefaultOptions, CancellationToken.None);
    }

    public static Task RunAsync(this IWidget obj, CancellationToken token)
    {
        return obj.RunAsync(DefaultOptions, token);
    }

    public static Task RunAsync(this IWidget obj, Options options)
    {
        return obj.RunAsync(options, CancellationToken.None);
    }
}

Now we have all the convenience methods the user needs without the burden of boilerplate for the implementer. Better yet, since the extension methods operate against the interface and not the individual implementations, we can write exactly one set of tests and be done with it. All implementations, by definition, will expose the functionality in exactly the same way, and have a much lower chance of doing it wrong.

For a real-life example of this pattern, check out Microsoft.Extensions.Logging.LoggerExtensions. Instead of defining upwards of two dozen LogXxx methods directly on ILogger, there is just one Log(...) method to implement which the extension methods can forward to.

Are you suffering from overload overload? See if extension methods can ease the pain.

Leave a Reply

Your email address will not be published. Required fields are marked *