DEV Community

Paulo Pozeti
Paulo Pozeti

Posted on

Interface definition has never been so important!

Problem

I know you might be looking at this and thinking: interface definition has always been important! And I agree to that, but that's not what I'm seeing on the ground.

With the increase of tutorials of prescribed architectures (hexagon, clean...), it's clear to see that the only intent for interfaces now is to create a boundary between layers, instead of a boundary of meaning.

A boundary between layers says 'this code lives here.' A boundary of meaning says 'this is what I can do and why.'

What I mean by that? Well, lets look at a simple example I found on the web:

// https://github.com/jasontaylordev/CleanArchitecture/blob/main/src/Application/Common/Interfaces/IApplicationDbContext.cs
public interface IApplicationDbContext
{
    DbSet<TodoList> TodoLists { get; }

    DbSet<TodoItem> TodoItems { get; }

    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
Enter fullscreen mode Exit fullscreen mode

IApplicationDbContext doesn't have a specific job. Its only purpose is to give you a limited number of actions against EF DbSet's. And if you can't see any usings in that about EntityFramework is because that is hidden via global usings.
So all they are doing is "I'm going to define an interface, that doesn't abstract anything, doesn't have any meaning other than represent some other layer's capabilities".

Why does this matter now?

I don't know if you noticed, but we are entering a new phase of software engineering. We are shifting from trying to find existing NuGet packages that does the work we need to now use AI that generates the minimum amount of code needed to suffice our requirements.

But the problem is that AI often does two things:

  1. Perpetuate the "meaningless" interface approach
  2. Might entangle your code with other packages

To me, the 2nd point isn't as critical because that happens nowadays anyway. But the 1st point we must think about and address properly!

Solution: meaningful interfaces

I picked the first example I could find on the internet when I searched for "clean architecture template", luckily that is also the easiest one to fix!

If we look back at IApplicationDbContext, and I didn't mention it as a problem, but we can see that it holds two meanings:

  1. State management: creating/updating saving changes
  2. Queries: accessing the raw DbContext to do whatever query

Since that interface holds two meanings we already know what we gotta do, let's split them.

State management abstraction

First lets create a new interface and name it very explicitly:

// Explicit intent which is to handle Todo's state. We can only pull the ToDo or add one. Maybe in the future we can also remove it.
public interface ITodoStateData
{
    void NewTodoList(TodoList newItem);

    TodoList GetTodoList(int id);
}

// Saving changes isn't part of the ITodoStateData, it belongs to something else.
public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

Enter fullscreen mode Exit fullscreen mode

As you can see, now those interfaces have a whole new meaning. Just by looking at the name of the interface you can grasp what they should be doing.

Then when you implement those, you can go nuts on EF capabilities.

An example would be:

public class ApplicationDbContext : DbContext, ITodoStateData, IUnitOfWork
{
    // Here you would have your dbsets as usual...
    public DbSet<TodoList> TodoLists => Set<TodoList>();
    public DbSet<TodoItem> TodoItems => Set<TodoItem>();

    // ITodoStateData members
    public void NewTodoList(TodoList newItem) => TodoLists.Add(newItem);
    public TodoList GetTodoList(int id) => TodoLists.First(x=>x.Id == id);

    // IUnitOfWork SaveChangesAsync just works out of the box
}
Enter fullscreen mode Exit fullscreen mode

That only solves the state management problem, now let's go to the query one.

Queries

For queries, many people are now thinking that exposing the DbContext is the best approach as it gives you most of the flexibility. That is true but it doesn't give you reusability and meaning.

To me, queries can fall into two buckets:

  1. It fulfills an API request or all the data for a specific view
  2. It's a shared piece of logic that must be true all the time

The approach of having DbContexts is fine for the bucket #1 but falls short on #2.

How I would solve this problem is by, guess what, creating more interfaces with meaning!

// When you read: Todos list page, what comes to your mind?
public interface ITodosListPage
{
   Task<PaginatedList<TodoItemBriefDto>> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken) 
}
Enter fullscreen mode Exit fullscreen mode

So now, that interface is very specific to the page that shows a list of Todos!
Now where you implement this? Can be in ApplicationDbContext itself, or another class in the Infrastructure project that has access to ApplicationDbContext.

If you have sharp eyes you might ask the question: hold on, you are passing GetTodoItemsWithPaginationQuery down to Infrastructure?
Ok, so you are paying attention. How you solve this is by creating a more general abstraction for the "pagination data" that your infrastructure can fulfill. I shall write about this later!

Conclusion

We should be creating interfaces to ensure proper meaning between layers and also be very explicit about their intent!
It's completely fine to have smaller but full of intent interfaces. Then have all of those implemented by a single class. Especially when they are all tied to external packages like EF!
Another thing worth saying is that once you've set up this pattern in your code base, AI will definitely pick this pattern up and give you more aligned solutions.
As with anything, this doesn't come for free. More interfaces mean you may end up with multiple implementors of the same logic. But to be honest, this already happens with the queries being duplicated everywhere anyway.

Top comments (0)