P3.NET

What Is Wrong With My Interface

It happens all the time. A developer needs some functionality in their application so they start writing an interface and then an implementation to go with it. Then something doesn’t compile, the code doesn’t work right or they cannot figure out how to get their design to work. Next comes the inevitable question “How can I fix my interface?” The answer is “fix your interface by throwing it away.” To be fair it isn’t generally the developer’s fault they went this way. We are trained from an early (developer) age to write code like this. We hear about it in school. We go to our first job and they are using interfaces everywhere. The problem is it is the wrong approach. In this article I’m going to discuss why we (generally) have it backwards and how you can start doing things “the right way”.

Before getting started please note that this is an opinion piece based upon my years of writing code “the wrong way”. If you search for other opinions you will likely find others that agree with me. There has been a slow transition back away from interfaces in the last couple of years and I believe it will continue to grow.

Design Patterns

To understand why interfaces are generally the wrong approach we need to first understand what an interface is designed to do. This is where design patterns come in. A design pattern is a generally reusable pattern for solving common problems. Design patterns are developed as we find standard solutions to common problems. Here’s a couple of examples.

Facade

The facade pattern is a structural design pattern used to simplify code that is otherwise complex to use. As we build more and more complicated software the dependencies between components can get overwhelming. A facade pattern can be used to simplify this complexity by wrapping the complex system into a simpler one that has fewer dependencies. This can be done any number of ways but ultimately the code that relies on a facade is using a simpler interface compared to the original.

Interfaces are often used here because interfaces allow us to expose whatever facade we want over an existing type. The underlying type is still there and available but calling code (using the interface) only works with the interface. Dependency injection is a common pattern used to combine complex types with their facades. Here is a simple example.

interface IPaymentService
{
   PaymentResponse MakePayment ( PaymentRequest request );
}

public class EnterprisePaymentService : IPaymentService
{
   public EnterprisePaymentService ( IPaymentDatabase database, IPaymentProvider[] providers )
   { ... }

   public IUser CurrentUser { get; set; }

   public PaymentResponse MakePayment ( PaymentRequest request )
   {
      foreach (var provider in providers)
      {
         if (!provider.AuthorizePayment(CurrentUser, request))
            return PaymentResponse.NotAuthorized();
      }

      ...
   }

}

Adapter

The adapter pattern is also a structural design pattern. It is used to adapt one interface to another. This is generally done with accessing third-party systems that need to work with a company’s systems.

interface ILogger
{
   void Log ( string message );
}

public class Logger : ILogger
{
   public Logger ( RealLogger <span class="hiddenGrammarError" pre="">logger )
   {
      _logger</span> = logger;
   }

   public void Log ( string message )
   {
      _logger.LogEntry(LogLevel.Information, message);
   }

   private readonly RealLogger _logger;
}

Interfaces as Abstractions

An interface is an abstraction mechanism that we use when we want to separate an implementation from its usage. It is a decoupling technique. This was the original purpose of interfaces and it is why you’ll often see interfaces mentioned in design patterns. The problem is that we’re forcing abstraction into areas that don’t need it just for the sake of abstraction. Interfaces are one approach to abstraction, not the only approach. Here’s another very common example that is, in most people’s minds, wrong.

interface IRepository<T>
{
   void Add ( T item );
   T Get ( string id );
   void Remove ( T item );
   void Update ( T item );
}

How did we get here? It generally starts like this “we want to store our data in database X but we want to be able to switch it out later if we need to.” That’s the first sign something is wrong – YAGNI. Are you currently looking at different databases? Is there a business need to use a different database later? Have you ever switched a system from one database to another and the ONLY change you needed to make was with the database layer? I’ve been writing software a long time and this has never happened. If it has to you then I think you’re in a minority.

Interfaces as Abstracted Implementations

The other issue with the “generic repository” pattern is that you’re applying abstraction at the wrong level. I don’t know about your systems but in our systems there are things our apps can CRUD (e.g. orders), but there are lots of other things they cannot (e.g. states in the US). By creating a generic repository you are trying to apply abstraction to all database objects rather than abstracting what you can do with each object.

This is where interface-based programming leads you. Everything is an interface so you create your first interface before you even have an idea of what the implementation would look like or even be used for. Somebody once said that you should never create an interface without also creating an implementation. I’m going to go further and say you shouldn’t create an interface until you have at least two different implementations. Here is how interfaces tend to be developed.

  1. An interface is created for the system to use.
  2. The first implementation is added.
  3. Additional members are added to the interface as the system needs more functionality.
  4. At some point some additional information is needed to get the implementation to work so the implementation is updated.
  5. The interface no longer matches the implementation so the interface is updated.
  6. More changse are needed so the implementation and interface are updated some more.

What you have at this point is an implementation and an abstract interface that backs it, not the other way around. In a perfect world you have an interface and the implementation molds itself to fit the interface. This doesn’t tend to work out if you start with a single implementation.

The guideline for having at least two implementations will force developers to evaluate the true similarities between the implementations. The common stuff is the interface. Everything else is an implementation detail. If you cannot find enough similarities between two implementations then there is really no interface that will work. This also indicates where, perhaps, more than one interface might be needed.

Interface Challenges

Up to this point we could technically argue that all abstraction can fall prey to these concerns. Where interfaces are different is in their complexity, both in how they are used and how they are written. Some of these issues are very much specific to the language being used. The two primary languages I have worked with in my career are C++ and C#. C++ doesn’t have true interfaces so we’ll focus on C#.

Immutability

The biggest issue with interfaces is that they are immutable. For private interfaces you could ignore immutability but public interfaces cannot be done this way. In order for your code to rely on an interface you must be guaranteed that ALL implementations follow the same contract. That is, after all, the whole purpose of interfaces. Once an interface is “released” it cannot be changed without breaking all existing implementations. Imagine if Microsoft suddenly added even one new member to IEnumerable. All existing implementations would be broken. That cannot happen so interfaces are effectively immutable. Even if Microsoft wanted to add new members and the entire developer community was OK with it all existing code would need to be updated.

The only “workaround” for immutable interfaces is versioning. This was the common solution in the COM-era. But this is really just a workaround to the problem. Code that wanted to use the “new” functionality had to be changed to look for the new version. In reality you could have created a completely new interface and the same changes would have been needed.

With .NET we do have the ability to provide extension methods on interfaces. In some ways this helps work around the immutability but in reality it is just providing helper methods to existing implementations. Implementations cannot influence or change the extensions nor can the extensions do anything a developer couldn’t already do. So in some ways they are just as limiting.

Implementation Differences

Another issue with interfaces is they are truly just abstractions over interfaces. An interface has no control over the actual implementation and therefore any interface rules may or may not be enforced. Take a look at the IComparable.CompareTo method documentation. There are a series of rules for an implementer to follow. Guess what, if an implementer either does not know or chooses to ignore the rules then the code will still compile. Unless you write tests against each implementation you rely on then there is no way to know the implementation “is not compliant”.

Even for interfaces that don’t provide rules around how an implementation works calling code has to handle differences in implementations. Take this simple example.

interface IDatabase
{
   MyObject Get ( int id );
}

What gets returned if the object cannot be found? null perhaps or maybe it’ll throw an exception. The interface can specify what it believes is the correct approach but, again, the implementation can do whatever it wants.

Simplifying Unit Testing

This is the big one for a lot of people. Whenever you hear arguments for and against interfaces they eventually get around to testing. Given mocking frameworks it is very easy to test code that relies on interfaces. Just point the mock to the interface and it is done. The problem is that you’re testing the mock, not the implementation. There is no question that interfaces CAN make writing unit tests easier but I’d argue that other abstraction approaches do the same thing.

Abstract Classes vs Interfaces

So if interfaces aren’t (always) the right approach to abstraction then what else do we have? The answer is classes, generally abstract. Most languages that don’t support interfaces do tend to support abstract classes. So even in languages like C++ you can use abstract classes just like interfaces. In fact abstract classes are better for encapsulation in many ways than interfaces.

So what is an abstract base class. An abstract base class is simply a class that is designed for inheritance and cannot be instantiated on its own. Interestingly enough it has all the same advantages as interfaces with the added bonus of default implementations.

Consistent Implementations

With an interface you have to define all the members. With an abstract class you can provide default implementations of some members while leaving others for derived types to implement. The benefits are plentiful.

  • Control which parts of the implementation are changeable.
  • Provide default implementations for functionality that should generally work the same.
  • Force implementations to provide code for areas that are truly implementation specific.

Here’s how you might try implementing the IDatabase interface of earlier.

public abstract class Database
{
   public MyObject Get ( int id )
   {
      //Validate ID

      return GetCore(id);
   }

   protected abstract MyObject GetCore ( int id );
}

In this example the abstract class provides a default implementation that enforces the common rules for the interface while still requiring an implementation to provide the core logic.

Mutability

Another benefit of abstract base classes is they can be mutable. Suppose I want to add support for getting multiple items from the database. With an abstract class I can add new members without breaking the existing code PROVIDED I do not introduce any new abstract members. But the “new” members can be overridable if we want.

public abstract class Database
{
   public MyObject Get ( int id )
   {
      //Validate ID

      return GetCore(id);
   }

   public IEnumerable<MyObject> GetAll ( params int[] ids )
   {
      //Validate IDs

      return GetCore(ids);
   }

   protected abstract MyObject GetCore ( int id );

   protected virtual IEnumerable<MyObject> GetAllCore ( int[] ids )
   {
      foreach (var id in ids)
         yield return GetCore(id);
   }
}

In this implementation the base class provides a reasonable default implementation. However since the GetCore method is virtual a derived type may choose to implement a more efficient version instead. In either case the users of the class have a new method to use without having to worry about broken code.

Simplifying Overloads

When implementing an abstract base class it is generally a good idea to require a single abstract implementation of each method. But that doesn’t mean you cannot support overloading.


public abstract class FileSystem { public IEnumerable<string> GetDirectories ( string path ) => GetDirectories(path, option); public IEnumerable<string> GetDirectories ( string path, SearchOption option ) { //Validate parameters return GetDirectoriesCore(path, option); } protected abstract IEnumerable<string> GetDirectoriesCore ( string path, SearchOption option ); }

While this is a simple example notice that we have multiple overloads that call the same abstract member. A derived type need only implement the, generally most generic, version while we publicly provide various overloads to make it easier for calling code to use. Provided we don’t change the abstract member in the future we can add additional overloads if needed later.

Composition

At least in C# we can have a single base class. Sometimes we run into situations where an implementation must derive from a pre-defined base type. In that case an abstract base class is not going to work. Fortunately though we have the composition pattern. In this pattern we take the implementation we want and we wrap it in another type. The example given for the adapter pattern was using composition to wrap a type that may have had its own base type requirements. With composition we can easily work around the limitation of C# while still being able to use abstract base classes.

Abstraction as a Refactoring Tool

At this point it may sound like I am against abstraction but quite the opposite. Abstraction is a powerful tool for decoupling code making it easier to maintain over the long term. But anybody who has ever used abstraction knows it adds complexity. At a minimum you’re dealing with an implementation type and the abstract type (interface or abstract base class). Would you willing add complexity to your code just for the sake of adding complexity? I would hope not. Like all things the goal should be the simplest solution that solves the problem. This is one area where I believe red-green testing has the right idea. Write the simplest code that solves the problem you are trying to solve. Refactor up the chain as needed.

Here’s another question – do you abstract away calls to the file system? How about time such as when a record is created or updated? Why not? The answer is probably no. You’ve never really needed to because all systems have a time and almost all have some sort of file system. But unit testing such code tends to be harder. We should have this same mentality for all our code. Abstraction is a refactoring tool so use it as such. Think about how you refactor code. There are plenty of good books about refactoring.

All of them say the same thing though – refactor EXISTING code.

  1. Write code in the exact spot where you need it.
  2. If you need to reuse that code elsewhere in the same file then promote it to a function.
  3. If you need to reuse that code in other files then promote it to its own type.
  4. If you need to reuse that code in other projects then promote it to a library.

Abstraction is a refactoring tool. Refactor up to abstraction, not down.

  1. Start with no abstraction. Write (and use) the implementation you need to solve the exact problem.
  2. If you need a different implementation then write a new implementation type to solve the new problem.
  3. If you need yet another implementation then move the shared functionality into an abstract base class. Refactor the original implementations to use the new class.
  4. If an abstract base class cannot be used then use an interface.

Interfaces Aren’t Evil

Given their limitations an abstract base class is the right answer in most cases. But there are some cases where an interface would be the better choice and some of these cases are simply because of C#.

  • If you need to be able to apply abstraction to value types then an interface is the only option.
  • If you find that your abstract base class has no default implementation of anything then an interface (may) be the better choice.
  • If the base type is already in use and composition cannot be used then an interface is the only option but this would be a rare case.
  • If the relationship isn’t an is-a relationship then an abstract base class probably isn’t the right choice.

This last one is an especially interesting scenario. Like interfaces we developers have been taught to think of everything in an object-orient (OOD) way. Guess what – that is a bad idea. OOD should be used for business modeling, not for everything. There are many cases where OOD like inheritance isn’t the best choice. Data transfer objects and UI models tend to fall into these categories. One place where interfaces tend to be used is with optional functionality. As an example .NET has the IValidatableObject interface. The purpose of this interface is to validate an object. Types implement this interface when they want to expose the fact they can be validated. The [IComparable]https://docs.microsoft.com/en-us/dotnet/api/system.icomparable-1) interface is used when a type supports comparison. These are examples of interfaces that signify support for functionality. An abstract base type wouldn’t make sense here because it isn’t a relationship. The interfaces are identifying functionality, not purpose. In this case an interface is the best choice.

Final Thoughts

Interfaces are not inherently evil. When used properly they help solve real problems in an elegant manner. But they introduce unnecessary layers of abstraction and complexity when over used. Our current programming culture has us trained to use them without thinking. Instead we should be refactoring to interfaces only when we find the need for them. This will help us keep our code as simple as possible while still being able to refactor out to interfaces if and when they provide true value.