How To Hide A Leaky Abstraction In Plain Sight 🔊
Thoughts on how to ease the negative impact of an abstraction leak
The Law of Leaky Abstractions states that:
All non-trivial abstractions, to some degree, are leaky.
When we try to consume an abstraction to hide complexity:
- If the abstraction is built on top of a dependency that makes impossible to achieve certain features.
- Or if the abstraction is built on top of a dependency and that dependency has less power but implements part of a fundamental capability that the abstraction doesn’t have.
… then something is gonna leak.
An efficient abstraction is the one that doesn't let any characteristics of its internals influence in the result presented to the user. It should be totally hidden. Failing to do that is a violation the Principle of Least Astonishment due to the fact it's exposing unexpected side-effects to the users.
However, there is a way to prevent an abstraction from exposing unexpected side-effects to the users:
Make them expected!
If the abstraction is aware of the leaks, it can make the dependency as part of the contract without mentioning its underlying implementation. It can use the abstraction as if the leaked component was naturally a part of it, as long as the user can't perceive the difference.
Wikipedia has a detailed definition of what a leaky abstraction is:
In software development, a leaky abstraction is an abstraction that exposes to its users details and limitations of its underlying implementation that should ideally be hidden away.
The key to this definition here is the part "that exposes to its users details and limitations of its underlying implementation that should ideally be hidden away". It will not be leaky if the abstraction "exposes to its users details and limitations of itself".
An abstraction leak happens when it exposes details and limitations of its underlying implementation to the user, not when it can, instead, successfully expose details and limitations of itself.
If the leak is part of the abstraction, then it's not exposing internal things to its users that should be hidden. The user does not even know that there is an underlying implementation in the first place. It might be an abstraction leak to the author of the abstraction, but not to the user, for that an abstraction leak only happen when the internals are exposed to its users, not the author.
For example, let's say there is a library that contains the function
random(). It's documented as "a function that generates a random number". The consumer realizes later, after a costly debugging process, that the number is not random under certain circumstances because of the algorithm being used internally to generate randomness. The abstraction allowed the algorithm to be leaked to the user.
Let's say that another similar library also contains the function
random(). It's implemented the same way and has the same leaky behavior. But it documents the contract as "a function that generates a random number under circumstances x and y". The user understands it might not be random if the circumstances don't match and they consume the function aware of how it works. The underlying algorithm was not unexpectedly leaked to the user, it became encapsulated in the abstraction itself.
Just because the contract has the behavior of the leaked abstraction, it doesn’t mean that something inside the abstraction was in fact leaked by the eyes of the user. The dependency with a weak capability might be the only way to implement the solution, but what the user perceives is just a behavior that cannot be changed, even though it can seem counterintuitive.
The leaky abstraction was hidden in plain sight.
Exposing an abstraction leak might be the most effective solution to hide it
If the abstraction is aware of what can leak, then it is totally reasonable to expose that to the consumer so that they can take the necessary precautions to handle it — like to consider the limitations into the decision to use the
random() function or not. The abstraction will also benefit from it because it will be creating a contract that holds true, even if it's counterintuitive.
Besides, this practice can encourage both parties (the A.P.I. author and the A.P.I. consumer) to focus on the communication between the problem and the solution. This works very well with how some Open Source projects document their API.
If an abstraction can successfully hide its leaked source to the user, then for all practical purposes the leak doesn't exist.
Documenting the contract won't prevent the abstraction from leaking to the author, but at least it will prevent from unexpectedly leaking to the user.