Interfaces are contracts too

Published on 5/24/2016

Lately I’ve noticed that while everyone’s talking about SOLID principles, it also seems that most people think that the benefit of “I” (Interface segregation) is solely related to dependency injection. While that is indeed one benefit, the true (and original) intention of an interface is often overlooked; that is, an interface is first and foremost a contract.

An interface is not just a collection of methods or functions. The declaration creates an expectation with the consumer of the interface, whether it is an API, a DLL, another layer in your project or a factory pattern. Firstly, there is an expectation of implementation, and secondly an expectation of terms.

Let’s talk about implementation. In Visual Studio for example, when you use the IDE function to implement an interface, you usually end up with generated code that looks like this:


    public class Foo : IComparable
    {
        public int CompareTo(object obj)
        {
            throw new NotImplementedException();
        }
    }

This is, in my opinion, a very dangerous template result. It leaves the code in a state that builds and requires no further action to be taken. It’s obviously not really an issue with a simple interface like IComparable since there is only one method to implement, and you probably used IComparable for that method in the first place. But what if it’s an interface that defines plenty of methods?


public class Foo<T1, T2> : IDictionary<T1, T2>
    {
        public T2 this[T1 key]
        {
            get
            {
                throw new NotImplementedException();
            }
        set
        {
            throw new NotImplementedException();
        }
    }

    public int Count
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    public bool IsReadOnly
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    public ICollection&lt;T1&gt; Keys
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    ...

I’ve seen implementations of interfaces that only implement the immediately required methods “to get it working” and leaves the rest all as “not implemented”. This is particularly bad behavior because anyone else who looks at Foo through the Object Browser or intellisense will immediately grasp that Foo implements a Dictionary and can therefore be used as such. But it doesn’t. Introducing a half-implemented interface is tremendously dangerous and will most certainly introduce huge project stalls further down the road. You as a developer are not honoring the contractual nature of an interface if you do this. There is only one exception to this rule, and that is when you create a fake for unit testing and only need a specific method. However if that method is extendable (virtual or abstract) rather inherit from a properly implemented fake and override that one method, keeping the contract in place.

The second aspect of this contractual nature is the terms of the contract. By this I refer to the declared methods and, importantly, also the method signatures; this is something a lot of people overlook. Altering an interface introduces immediate breaking changes to everyone downstream of you, which is essentially breaking contract (and which should be punishable by death). This is why most web-based APIs introduce a version routing identifier, most commonly in the form of /v1/. The reason they do this is to preserve the contract on version 1, while introducing significant new or altered contract terms for version 2. But so often I come across an oversight of the signatures; the parameter and output objects. Anything that forms part of the interface declaration is part of that contract. This includes classes used either as the result or as parameters of methods. And it includes all the properties and classes used inside those. You can see how quickly this rabbit hole goes fairly deep, which is exactly why you have to make use of properly structured domain models, or better yet, interface models declared specifically for use in the contract. You are steering into disaster if you use your ORM entities directly in your interfaces.

And while that still seems pretty obvious, let’s talk about the persistence of these classes used in the signatures. For example, let’s declare the following interface and class:


    public interface IFoo
    {
        FooPrinciple Login(string username, string password);
    }
public class FooPrinciple
{
    public string Username { get; set; }
    public string Domain { get; set; }
    public DateTime Expiry { get; set; }
}

Without API documentation it’s unclear to the user of an instance of Foo : IFoo that Expiry is local time or UTC time. And, changing that behavior in your code, as the implementer, could have big repercussions to everyone relying on this contract. Clearly, using a more explicit name for that property should be considered, but it’s also important that the meaning of Expiry stays the same throughout the life of the interface.

Looking at a more complicated example, let’s change IFoo to this:


    public interface IFoo
    {
        FooResponse<FooPrinciple> Login(string username, string password);
    }
public class FooResponse&lt;T&gt;
{
    public string Message { get; }
    public int Code { get; }
    public T Data { get; }
}

public class FooPrinciple
{
    public string Username { get; set; }
    public string Domain { get; set; }
    public DateTime Expiry { get; set; }
}

Typically a user of an instance of Foo : IFoo will expect that once the Code property indicates success, the Data property will be set. No null checking should be necessary because clearly it’s part of the interface, and thus the contract. In the past I’ve received responses like “Just look at the Code property, the Data property is not set anymore”. This is awful, and is completely breaking the contract established by the interface. If in some cases the Data property is set and in other cases not, then the pattern is a bad design and should be abstracted from the interface. If Data was being set previously and then is suddenly not anymore, the authors should be shot at dawn.

Contracts are a basic necessity of integration projects or phases, and breaking contracts costs everyone huge amounts of time and money, not to mention the tension that it creates between the different parties. Of course, depending on the project or even development phase, you don’t have to introduce a new interface every time there are some changes. By using interfaces correctly, breaking changes will become apparent to the integrator through breaking builds, and as long as the changed interface matches the expectations it sets, the need for detailed release notes is mostly negated.