Plugging Leaky Abstractions
Editorial Note: I originally wrote this post for the NDepend blog. You can check out the original here, at their site. While you’re there, download NDepend and play around with it.
In 2002, Joel Spolsky coined something he called “The Law of Leaky Abstractions.” In software, an “abstraction” hides complexity of an underlying system from those using the abstraction. Examples abound, but for a quick understanding, think of an ORM hiding from you the details of database interaction.
The Law of Leaky Abstractions states that, “all non-trivial abstractions, to some degree, are leaky.” “Leaky” indicates that the abstraction fails to adequately hide its internal details. Imagine, for instance, that while modifying the objects generated by your ORM, you suddenly needed to manage the particulars of some SQL query. The abstraction leaked, forcing you to understand the details that it was supposed to hide.
Spolsky’s point may inspire a fatalistic feeling. After all, if the things are doomed to leak, why bother with them in the first place? But I like to consider it a caution against chasing perfection rather than a lament.
Abstractions in software help us the same way figurative language helps our prose. Metaphors and analogies offer ease of understanding, but at the accepted price of lost precision. If you press a metaphor enough, it will inevitably break down. But that doesn’t render metaphors useless — far from it.
Thus, if you have a leaky abstraction, you can take steps to “plug” it, so to speak. Spolsky says it himself, right in the law he coined: “all non-trivial abstractions are, to some degree, leaky.” We have the ability to lessen that degree.
Offer a Consistent Abstraction
Let’s consider an imaginary API that let you model a cooking activity. It offers the following methods.
- Fry(Food food)
- Bake(Food food, int temperature)
- Grill(Food food)
- Microwave(Food food, int seconds)
Now the API author receives a requirement that the application should also be able to support boiling. Let’s also suppose that the developer to implement the new feature pays little attention to the previous API. He implements a Pot object and a Water object with which API consumers must contend. They must instantiate a pot, fill it with water, and then call Pot.Add(Food) before calling Pot.Boil(). If water has not been added, the Pot.Add(Food) method throws an exception. Likewise, if the pot lacks water and food, Boil() throws an exception.
If the author had followed the previous pattern, however, life would be simpler.
- Boil(Food food)
Instead, this new incarnation of the abstraction forces details on its users. These details create leakiness. Before the new author put his implementation together, it was fair to say that the details of cooking (adding ingredients, device operation details, etc) were hidden. The implementation of boil torpedoes this hiding.
To plug leaky abstractions, start by ensuring that you offer the same level of abstraction across the board. Forcing details on users in a pocket of your API is probably the easiest way to leak.
Introduce a Secondary or Alternate Abstraction
Let’s return to the example above, with a slightly different take. Assume that, for whatever reason, Boil(Food) creates fundamental difficulty in implementation, rendering it impractical. You could simply not consider it part of the same abstraction.
Packaging matters when it comes to abstractions. And you can now look at it as offering two abstractions, rather than one.
- Simple Kitchen Operations
- Detailed Kitchen Operations
All of the original functionality falls under the “simple” heading while the new, implementation of boiling falls under “detailed.” And now, “simple kitchen operations” no longer leaks the way it did earlier.
To understand this, look no further than your microwave at home. Most microwaves these days offer the “traditional” interface where you select cook time and power level, but also “quick” options, such as “popcorn” and “reheat pizza.” Those latter options involve a single button press only, simplifying common scenarios.
But we do not think of these as leaky abstractions. Rather, we think of these as a different operating mode. Managing abstractions and preventing them from leaking is largely a matter of managing user expectations and understanding.
Embrace the Leak — Incorporate It into the Abstraction
Another, similar strategy for handling leaks involves actually addressing them in the abstraction. In the ongoing kitchen example, this would involve clarifying that boiling somehow presented an operation requiring more granularity. But that doesn’t really track well here, so I’ll switch gears to something more technical.
Consider the case of lazy loading. Lazy loading involves abstracting from users the details of making a resource accessible. The user just calls Thing.Get() and gets a thing. Behind the scenes, Get() will either have a cached reference to the thing or it won’t, but all of this remains hidden from the user.
As I’ve mentioned previously, lazy loading presents some issues to clients and maintainers. Most prominent among these is the opacity of operation times, which often matter to users.
We might thus view lazy loading as an inherently leaky abstraction. It purports to hide details from callers, but, in reality, callers need to know whether or not lazy loading occurs behind the scenes in order to account for orders of magnitude in performance variance. When consuming, we generally look for context clues such as singleton implementations or code comments.
It then bears asking, “why hide this detail in the first place?” Would it be so terrible to offer an API with, for instance, the following concepts?
- bool IsLoaded
- void LoadFromSource()
- void GetFromCache()
Sure, three interaction points replace a single one. But the details of caching and loading remain hidden, and indeterminate performance times will no longer flummox callers. They will get exactly the performance they ask for.
Just Get Rid of It
The last point may seem a bit fatalistic, after I promised optimism in the introduction. But try not to think of it this way. Instead, realize that abstractions offer value, but not all of them and not always.
It has happened to me, and I imagine that it’s happened to you. Sometimes, you realize that an abstraction you’re attempting to offer creates more confusion than it saves. Or sometimes you realize that an older abstraction has outlived its usefulness. If you find this happening, consider a strategic retreat.
This may be a touch controversial, but I would argue that Webforms offers an example of the latter — an abstraction that has outlived its usefulness. 15 years ago, it provided a neat way for a generation of desktop developers to move impressively seamlessly to web development. In a modern, web-centric world, it muddies the waters considerably for anything but simple forms and grids. When this happens, the best abstraction can sometimes be no abstraction. A nonexistent abstraction cannot leak (or confuse anyone).
Abstractions are one of the most important tools in the developer tool belt. We create them routinely and often subconsciously. But as we create them, we must contemplate their use and do everything in our power to make that use go well. So take some time and plug your leaky abstractions.
Is this why so many layered architectures aren’t?
🙂