C# – Is an interface exposing async functions a leaky abstraction

api-designcdependency-injectioninterfacesobject-oriented

I'm reading the book Dependency Injection Principles, Practices, and Patterns and I read about the concept of leaky abstraction which is well described in the book.

These days I'm refactoring a C# code base using dependency injection so that async calls are used instead of blocking ones. Doing so I'm considering some interfaces which represent abstractions in my code base and which needs to be redesigned so that async calls can be used.

As an example, consider the following interface representing a repository for application users:

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

According to the book definition a leaky abstraction is an abstraction designed with a specific implementation in mind, so that some implementation details "leak" through the abstraction itself.

My question is the following: can we consider an interface designed with async in mind, such as IUserRepository, as an example of a Leaky Abstraction ?

Of course not all possible implementation have something to do with asynchrony: only the out of process implementations (such as a SQL implementation) do, but an in memory repository does not require asynchrony (actually implementing an in memory version of the interface is probably more difficult if the interface exposes async methods, for instance you probably have to return something like Task.CompletedTask or Task.FromResult(users) in the method implementations).

What do you think about that ?

Best Answer

One can, of course, invoke the law of leaky abstractions, but that's not particularly interesting because it posits that all abstractions are leaky. One can argue for and against that conjecture, but it doesn't help if we don't share an understanding of what we mean by abstraction, and what we mean by leaky. Therefore, I'll first try to delineate how I view each of these terms:

Abstractions

My favourite definition of abstractions is derived from Robert C. Martin's APPP:

"An abstraction is the amplification of the essential and the elimination of the irrelevant."

Thus, interfaces aren't, in themselves, abstractions. They're only abstractions if they bring to the surface what matters, and hides the rest.

Leaky

The book Dependency Injection Principles, Patterns, and Practices defines the term leaky abstraction in the context of Dependency Injection (DI). Polymorphism and the SOLID principles play a big role in this context.

From the Dependency Inversion Principle (DIP) it follows follows, again quoting APPP, that:

"clients [...] own the abstract interfaces"

What this means is that clients (calling code) define the abstractions that they require, and then you go and implement that abstraction.

A leaky abstraction, in my view, is an abstraction that violates the DIP by somehow including some functionality that the client doesn't need.

Synchronous dependencies

A client that implements a piece of business logic will typically use DI to decouple itself from certain implementation details, such as, commonly, databases.

Consider a domain object that handles a request for a restaurant reservation:

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

Here, the IReservationsRepository dependency is determined exclusively by the client, the MaîtreD class:

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

This interface is entirely synchronous since the MaîtreD class doesn't need it to be asynchronous.

Asynchronous dependencies

You can easily change the interface to be asynchronous:

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

The MaîtreD class, however, doesn't need those methods to be asynchronous, so now the DIP is violated. I consider this a leaky abstraction, because an implementation detail forces the client to change. The TryAccept method now also has to become asynchronous:

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

There's no inherent rationale for the domain logic to be asynchronous, but in order to support the asynchrony of the implementation, this is now required.

Better options

At NDC Sydney 2018 I gave a talk on this topic. In it, I also outline an alternative that doesn't leak. I'll be giving this talk at several conferences in 2019 as well, but now rebranded with the new title of Async injection.

I plan to also publish a series of blog posts to accompany the talk. These articles are already written and sitting in my article queue, waiting to be published, so stay tuned.