The actual reason behind interfaces

iamprovidence
7 min readMar 27, 2024

--

No wonder that one of the most overused concepts in any programming language is interfaces. After all, they have been with us from ancient ages. Nowadays, the practice of adding them to every class has become so widespread that hardly anyone questions it. Often invalid reasons are ascribed to why an interface is introduced often resulting in a wrong usage.

In this article, we will expand on different reasons why an interface can be introduced to a codebase. You will see in which circumstances the use of an interface is preferable. But most importantly, you will find the actual reason behind interfaces.

Without any further ado, let’s begin

Expectations

I have been asking many developers why would they introduce interfaces in the code and in response they would just laugh at me.

“Are you kidding? It is an interface. Of course, we need one!”

Of course 🤓. You may not immediately tell why, but unconsciously feel like interfaces are handy and should be added. I am sure if you can strain the brain you will find a couple of reasons yourself. But I don’t want you to do it after a hard working day, so let’s just follow what I justify as good reasons.

Abstraction

The most basic reason to introduce an interface in your code is an abstraction.

Interfaces provide a way to abstract the behavior of an object from its implementation details.

interface IShape
{
double CalculateArea();
}

class Circle : IShape { . . . }
class Rectangle : IShape { . . . }

As soon as you have an abstraction, you have an inheritance. Inheritance means polymorphism. Polymorphism leads to OOP. With OOP we have patterns. That is where the fun starts 😎.

Contracts

Interfaces enforce contracts for classes they must fulfill. This exploits design by contract as opposed to design by convention.

For example, in ASP we can define a middleware class by conventions and by contract.

With a convention-based approach, you need:

  • a public constructor with a parameter of type RequestDelegate
  • a public method named Invoke or InvokeAsync. This method must:
    - return a Task
    - accept a first parameter of type HttpContext
  • additional dependencies can be added to the Invoke method
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;

public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext httpContext, ITextOutput output)
{
output.Write(DateTime.Now);

await _next(httpContext);
}
}

With a contract-based approach, you need to inherit and implement the IMiddleware interface:

public class MyCustomMiddleware : IMiddleware
{
private readonly ITextOutput _output;

public MyCustomMiddleware(ITextOutput output)
{
_output = output;
}

public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next)
{
_output.Write(DateTime.Now);

await next(httpContext);
}
}

That is how simple it is. No methods with magic signatures, nothing needs to be remembered or googled. I think you can tell yourself which approach is less bug-prone.

Loose coupling

If a class needs to be replaced or updated, as long as it adheres to the same interface, other parts of the code that depend on it won’t be affected.

For example, below we can easily change the implementation or rollback the the previous one. Without the interface, it would be much harder to achieve:

interface IUserRepository { . . . }

// used in POC
class InMemoryUserRepository : IUserRepository { . . . }

// used in production
class SqlUserRepository : IUserRepository { . . . }

// used when presenting to a customer 🙃
class MockUserRepository : IUserRepository { . . . }

Cross-cutting concerns

Interfaces introduce a point of extendability in your code.

Let’s say we have a repository. Having an interface allows us to extend the code without modifying it. For instance, we can decorate it with some kind of cache.

interface IUserRepository { . . . }

// responsible for user persistance
class SqlUserRepository : IUserRepository { . . . }

// responsible for a cache
// cache logic is encapsulated in a single place
class UserRepositoryCacheDecorator : IUserRepository
{
public IReadOnlyCollection<User> GetAll()
{
return _cache.GetOrAdd(key: "users", value: () =>
{
return _userRepository.GetAll();
});
}

public void DeleteInactive()
{
_cache.Invalidate(key: "users");

return _userRepository.DeleteInactive();
}
. . .
}

This is also known as the Open-Closed principle. When you add new code without modifying an existing one.

Generics

You may now be aware of it (because I just made up it myself 😅) but in C# there are four ways to define generics:

  • strings
  • objects
  • interfaces
  • generics

We are only interested in the last two.

In the same way generics allow us to write reusable code for different types:

interface IRepository<TKey>
{
void DeleteById(TKey id);
. . .
}

A similar can be achieved with interfaces:

interface IRepository
{
void DeleteById(IEntityKey id);
. . .
}

Still not convinced? Then try telling me the difference between those two:

void WriteToFile<T>(IEnumerable<T> collection, string fileName) 
where T : IShape
{
. . .
}

void WriteToFile(IEnumerable<IShape> collection, string fileName)
{
. . .
}

The marker interface

You can have an empty interface. This is so called a marker. Its purpose is simply to “mark” a class as having a certain capability or characteristic.

// marker interface
interface IEntity { }

class User : IEntity
{
. . .
}

Similarly to attributes they can be used to categorize or identify types of objects during runtime which behavior could be extended later with reflection

Dependency inversion

Interfaces allow us to invert dependencies in the way execution and dependencies flows are pointing in different directions.

This way your high-level modules depend on abstractions (interfaces), and concrete implementations depend on these abstractions too. This allows you to easily replace implementations without modifying the high-level modules.

Modularity

Modularity refers to the practice of breaking down a large system into smaller, more manageable parts or modules.

In the interface, you can define the expected behavior without digging into the implementation.

interface ICanFly { . . . }
interface ICanRide { . . . }
interface ICanWalk { . . . }


class Plane : ICanFly, ICanRide { . . . }
class Duck : ICanFly, ICanWalk { . . . }

// implementation details do not matter
// we only need to investigate ICanFly behaviour
ICanFly flyable = new ...()

Decomposition helps to isolate different parts of your codebase, making it easier to understand, maintain, and update.

Scaling

By designing your system with interfaces, you create a flexible structure that allows for easier expansion and modification…🥱

Alright, alright 😒I hope you haven’t gave up and closed the page at this point.

I can continue the list with other fancy words I found on the Internet. However, I know you clicked the link, not to read another boring article with obvious stuff. Same here. I also don’t want to list the same sh*t that every single article does 😏 So let me start again.

Reality

Regardless of what has been written, not many developers can explain why the left is better than the right:

Even though, what has been listed is technically correct those only apply in the OOP. Most of the time, the classes we have in the code are just anemic services or POCO classes that are closer to sets of functions and DTOs than to actual domain classes.

As practice shows, developers often introduce an interface for a database or any other abstractions, even when these interfaces have only one implementation:

interface IUserRepository { . . . }

class UserRepository : IUserRepository { . . . }

Sure, you are abstracting external dependency. But, at the same time, nobody can explain why there is no interface for Newtonsoft.Json, MediatR, AutoMapper, etc.

Interface with a single implementation neglects the whole point of abstractions and doesn’t provide loose coupling any more than concrete classes that implement those interfaces. Not a surprise interfaces leak the implementation details so often. Simply, because there is no second implementation, that would prevent us from doing so.

So what is the point of an interface then? 🤔

Unit tests.

Yeap. Regardless of how simple or stupid it sounds, the whole point of interfaces is unit tests. Without those, you would not be able to mock.

var mock = new Mock<IUserService>();
mock
.Setup(x => x.GetAll())
.Returns(new List<UserDto>());

var userService = mock.Object;

If it were possible to mock without interfaces you would see less of them.

Where I’m going with this is that you don’t need to define interfaces for every service. However, you probably still should do it, to make your code unit testable right away.

Of course, some advanced techniques, like inversion of dependencies, are simply not possible without those. But, I just want you to stop lying to yourself that you had an actual reason for the interface in your code.

💬 Let me know what is your thoughts about interfaces. Do you find them useful? Or do you think they add additional boilerplate for no reason?

👏 Give this article a clap if you liked it

☕️ You can also support me

✅ Follow

🙃 And keep coding

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence