10

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 ?

  • @Neil I probably got the point. An interface exposing methods returning Task or Task<T> is not a leaky abstraction per se, is simply a contract with a signature involving tasks. A method returning a Task or Task<T> does not imply having an async implementation (for instance if I create a completed task by using Task.CompletedTask I'm not doing an async implementation). Vice versa, the async implementation in C# requires that the return type of an async method must be of type Task or Task<T>. Put another way the only "leaky" aspect of my interface is the async suffix in names – Enrico Massone Jan 14 at 15:22
  • @Neil actually there is a naming guideline which states that all async methods should have a name ending in "Async". But this does not imply that a method returning a Task or a Task<T> must be named with the Async suffix because it could be implemented by using no async calls. – Enrico Massone Jan 14 at 15:25
  • 6
    I would argue the 'asyncness' of a method is indicated by the fact that it returns a Task. The guidelines for suffixing async methods with the word async was to distinguish between otherwise identical API calls (C# cant dispatch based on return type). At our company we've dropped it all together. – richzilla Jan 14 at 16:36
  • There are a number of answers and comments explaining why the asynchronous nature of the method is part of the abstraction. A more interesting question is how a language or programming API can separate the functionality of a method from how it is executed, to the point where we no longer need Task return values, or async markers? The functional programming people seem to have figured this out better. Consider how asynchronous methods are defined in F# and other languages. – Frank Hileman Jan 14 at 21:49
  • 2
    :-) -> The "functional programming people" ha. Async is no more leaky than synchronous, it just seems that way because we're used to writing sync code by default. If we all coded async by default, a synchronous function might seem leaky. – StarTrekRedneck Jan 15 at 21:28
7

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.

  • To my mind this is a matter of intent. If my abstraction appears as though it should behave one way but some detail or constraint breaks the abstraction as presented, that's a leaky abstraction. But in this case, I'm explicitly presenting to you that the operation is asynchronous - that's not what I'm trying to abstract. That's distinct in my mind from your example where I am (wisely or not) trying to abstract away the fact that there's a SQL database and I still expose a connection string. Maybe it's a matter of semantics/perspective. – Ant P Jan 17 at 13:13
  • So we can say that an abstraction is never leaky "per se", instead it is a leaky one if some details of one specific implementation leak out from the exposed members and constrain the consumer to change its implementation, in order to satisfy the abstraction shape. – Enrico Massone Jan 17 at 14:09
  • 2
    Interestingly enough, the point that you highlighted in your explanation is one of the most misunderstood points of the whole dependency injection story. Sometimes developers forget the dependency inversion principle and try to design the abstraction first and then they adapt the consumer design in order to cope with the abstraction itself. Instead, the process should be done in the reverse order. – Enrico Massone Jan 17 at 14:13
11

It's not a leaky abstraction at all.

Being asynchronous is a fundamental change to the definition of a function - it means the task isn't finished when the call returns, but it also means that your program flow will continue almost immediately, not with a lengthy delay. An asynchronous and a synchronous function doing the same task are essentially different functions. Being asynchronous is not an implementation detail. It's part of the definition of a function.

If the function exposed how the function was made asynchronous, that would be leaky. You (don't / shouldn't have to) care how it is implemented.

5

The async attribute of a method is a tag that indicates that particular care and handling is required. As such, it needs to leak out into the world. Asynchronous operations are extremely difficult to compose properly so giving the API user a heads up is important.

If, instead, your library properly managed all asynchronous activity within itself, then you could afford to not let async "leak` out of the API.

There are four dimensions of difficulty in software: data, control, space and time. Asynchronous operations span all four dimensions, thus, needing the most care.

  • I agree with your sentiment, but "leak" implies something bad, which is the intent of the term "leaky abstraction" - something undesirable in the abstraction. In the case of async vs sync, nothing is leaking. – StarTrekRedneck Jan 15 at 21:36
2

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

Not quite. An abstraction is a conceptual thing that ignores some elements of a more complicated concrete thing or problem (to make the thing/problem simpler, tractable, or due to some other benefit). As such, it is necessarily different from the actual thing/problem, and thus it's going to be leaky in some subset of cases (i.e., all abstractions are leaky, the only question is to what extent - meaning, in which cases is the abstraction useful to us, what is its domain of applicability).

That said, when it comes to software abstractions, sometimes (or maybe often enough?) the details we chose to ignore cannot actually be ignored because they affect some aspect of the software that is important to us (performance, maintainability, ...). So a leaky abstraction is an abstraction was designed to ignore certain details (under the assumption that it was possible and useful to do so), but then it turned out that some of those details are significant in practice (they cannot be ignored, so they "leak out").

So, an interface exposing a detail of an implementation is not leaky per se (or rather, an interface, viewed in isolation, is not in itself a leaky abstraction); instead, the leakiness depends on the code that implements the interface (is it able to actually support the abstraction represented by the interface), and also on the assumptions made by the client code (which amount to a conceptual abstraction that complements the one expressed by the interface, but cannot itself be expressed in code (e.g. the features of the language aren't expressive enough, so we may describe it in the docs, etc.)).

2

Consider the following examples:

This is a method that sets the name before it returns:

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

This is a method that sets the name. The caller can't assume the name is set until the returned task is completed (IsCompleted = true):

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

This is a method that sets the name. The caller can't assume the name is set until the returned task is completed (IsCompleted = true):

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

Q: Which one doesn't belong with the other two?

A: The async method is not the one that stands alone. The one that stands alone is the method that returns void.

To me, the "leak" here isn't the async keyword; it's the fact that the method returns a Task. And that isn't a leak; it's part of the prototype, and part of the abstraction. An async method that returns a task makes the exact same promise made by a synchronous method that returns a task.

So no, I don't think the introduction of async forms a leaky abstraction in and of itself. But you may have to change the prototype to return a Task, which "leaks" by changing the interface (the abstraction). And since it's part of the abstraction, it's not a leak, by definition.

0

This is a leaky abstraction if and only if you do not intend all implementing classes to create an asynchronous call. You could create multiple implementations, for instance, one for each database type you support, and this would be perfectly okay assuming you didn't ever need to know the exact implementation being used throughout your program.

And while you can't strictly enforce an asynchronous implementation, the name implies it should be. If circumstances change, and it may be a synchronous call for whatever reason, then you may very well need to consider a change of names, so my advice would be to do this only if you don't think this will be very likely in the future.

0

Here's an opposing viewpoint.

We didn't go from returning Foo to returning Task<Foo> because we started wanting the Task instead of just the Foo. Granted, sometimes we interact with the Task but in most real-world code we ignore it and just use the Foo.

What's more, we often define interfaces to support async behavior even when the implementation may or may not be asynchronous.

In effect an interface that returns a Task<Foo> tells you that the implementation is possibly asynchronous whether or not it really is, even though you may or may not care. If an abstraction tells us more than we need to know about its implementation, it's leaky.

If our implementation is not asynchronous, we change it to be asynchronous, and then we have to change the abstraction and everything that uses it, that's a very leaky abstraction.

That's not a judgment. As others have pointed out, all abstractions leak. This one does have a greater impact because it necessitates a ripple effect of async/awaits throughout our code just because somewhere at the end of it there might be something that is actually asynchronous.

Does that sound like a complaint? That's not my intent, but I think it's an accurate observation.

A related point is the assertion that "an interface is not an abstraction." What Mark Seeman succinctly stated has been abused a little.

The definition of "abstraction" is not "interface", even in .NET. Abstractions can take many other forms. An interface can be a poor abstraction or it can mirror its implementation so closely that in a sense it's hardly an abstraction at all.

But we absolutely do use interfaces to create abstractions. So to toss out "interfaces aren't abstractions" because a question mentions interfaces and abstractions isn't enlightening.

-2

Is GetAllAsync() actually async? I mean sure "async" is in the name, but that can be removed. So I ask again... Is it impossible to implement a function that returns a Task<IEnumerable<User>> which is resolved synchronously?

I don't know the specifics of .Net's Task type, but if it's impossible to implement the function synchronously, then sure it's a leaky abstraction (In this way) but otherwise not. I do know that if it was an IObservable rather than a Task, it could be implemented either synchronously or async so nothing outside the function knows and therefore it's not leaking that particular fact.

  • Task<T> means async. You get the task object immediately, but may have to wait for the sequence of users – Caleth Jan 17 at 9:29
  • May have to wait doesn't mean it's necessarily async. Shall wait would mean async. Presumably, if the underlying task has already been run, you don't have to wait. – Daniel T. Jan 17 at 11:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.