Another way to understand interfaces (C#)
One of the harder software engineering concepts to understand is that of interfaces. I once struggled to understand what interfaces are, how they work, and why a software engineer might use them. Most the explanations I could find on the Web didn’t make a lot of sense - some were more complicated than they needed to be, and others attempted to explain it in terms of other things I hadn’t grasped (i.e. dependency injection and inversion of control). Code should be elegant, straightforward and not require others to look through multiple classes to know what a method does. That is far more important than crafting ‘clean code’ for the sake of it.
My explanation might help. It might not. I’ll start it with something any developer would have seen a thousand times, and would no doubt have typed (or copied) and used without giving it much thought:
public IEnumerable<BlogPost> blogPosts { get; set; }
In this example, BlogPost is going to be an object that contains multiple properties, such as ‘Title’, ‘Content’, ‘Tags’, etc. But we want our code to handle multiple instances of it, so we’re making it an enumerable object, and calling that enumerable object ‘blogPosts’.
It would make sense, and would probably be just as valid to use Enumerable<>, the same way we’d use List<string>, but it’s far more common to see it declared as an IEnumerable<>, which is an interface.
Looking at the IEnumerable<> interface itself, we find it contains only references to methods that would make BlogPost enumerable. The references are empty methods that don’t do anything. The question becomes: Where is the code that actually turns BlogPost into an enumerable object?
Now this is where things might become a little confusing: The interface is rather like a class of (empty) methods, yet the parameters or objects passed to the interface would determine which of the methods would be executed.
It turns out the code that actually makes the object enumerable does exist elsewhere. The method is GetEnumerator(), and I’d imagine the class for that would look something like:
public class GetObjects : IEnumerable
{
public void GetEnumerator
{
// Code to get all objects
}
}
The interface only knows about this class and method because it has a reference to the latter, and because the class uses the interface (IEnumerable) as its base class. Actually a couple of my Unit of Work classes provides a clearer example of this:
public interface IUnitOfWork : IDisposable
{
IUserAccountsRepository UserAccounts { get; }
int Complete();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
UserAccounts = new UserAccountsRepository(_context);
}
public IUserAccountsRepository UserAccounts { get; private set; }
// Methods for committing changes and dispose
}
Perhaps the most obvious question is ‘Why on Earth would anyone call an interface instead of whatever method directly?’. I argue that one shouldn’t, unless an interface is actually needed. Code shouldn’t be made less readable and over-engineered, just for the sake of having what’s deemed ‘clean code’.
Interfaces are very useful when we’re building software with a context-dependent data source. For example, we want an application to use a database when running locally, and a different database when it’s deployed. Or we want the application to use mock data, instead of an actual data source, when we’re running unit tests against data access methods. Obviously, we’d likely have two different methods, in separate classes, that implement slightly different data access functions - one for a local database and another for the production database. But at the same time we shouldn’t be modifying the code to make it run in whichever context. The application should call the same thing (an interface) regardless of whether it’s running locally. e.g.
if(builder.Envirionment.IsDevelopment){
builder.Services.AddDbContext<IDataSourceConnector, LocalDataContext>();
} else {
builder.Services.AddDbContext<IDataSourceConnector, ProductionDataContext>();
}