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 depedencies
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.