In the article that popularized the leaky abstraction concept, Joel Spolsky claims that “all non-trivial abstractions, to some degree, are leaky.” Although I don’t disagree at all, I also feel that some abstractions leak way more than others. Why is this? I’m sure it depends on a lot of things, but I have one specific theory that I would like to present today. Let’s start, however, by briefly exploring what the idea is all about.
According to Wikipedia, a leaky abstraction is “an implemented abstraction where details and limitations of the implementation leak through.”
Abstractions aim to allow their users not to worry about details not directly related to the problem they’re trying to solve. You use a library, because you want your application to send an email, not to deal with various idiosyncrasies of SMTP. The library’s implementers, in turn, want a guaranteed mechanism for packet delivery, which is why they rely on TCP/IP. The protocol, then, doesn’t need to know whether the data gets transmitted by means of electrical signals, optical signals, or radio waves, as that is the responsibility of the network card’s device driver. I skipped a lot of layers and tiers – but I believe you get the idea.
You should, ideally, be able to ignore how things actually happen. You call the SendEmailMessage method, and the library guarantees a seamless delivery process. We don’t, however, live in an ideal world. Everything may work almost flawlessly when a user of your application is connected to the network in the office, but things become slow and unreliable when she’s using her cellphone while driving in excess of the speed limit on a remote rural highway. This is where the abstraction leaks, as you, the application developer, have to deal with the consequences of something you had hoped to abstract away from. (When the sending of a message fails, the user will expect your application to reattempt later, for instance.)
In his article, Joel Spolsky specifically uses TCP/IP as an example of a leaky abstraction. Although he’s generally not wrong, I would argue that this protocol belongs to “the better”, relatively “airtight”, less leaky abstractions.
I’m going to flip the question around: What constitutes an airtight (i.e., not leaky) abstraction (relatively speaking)? I believe such abstractions will share (to a greater or lesser extent) the following characteristics:
- The leaks are few and far between (again, relatively speaking)
- Working with the abstraction feels “natural”; you intuitively know what’s going on, even if you don’t understand exactly how
- Someone who doesn’t understand the underlying implementation can use the abstraction successfully (this may be the most important point)
Conversely, leaky abstractions will lack these characteristics. It goes without saying that everything is a question of degree: No absolutely airtight or absolutely leaky transactions exist. Rather, we need to place them on a continuum between two “ideal types”.
Essence and accident
Before I unveil my great secret theory, I need to introduce a little bit of philosophy. Specifically, I’m talking about Aristotle’s essence vs. accident distinction. (I really feel like being highbrow today.)
According to the Stanford Encyclopedia of Philosophy, “an essential property of an object is a property that it must have while an accidental property of an object is one that it happens to have but that it could lack.” In other words, the way I understand and interpret it, the essence of a concept is something you can’t take away from it without fundamentally changing what the concept is.
The idea was popularized in the software field by Frederick Brooks and his famous essay, No Silver Bullet – Essence and Accident in Software Engineering. In it, he postulates that “…software construction involves essential tasks, the fashioning of the complex conceptual structures that compose the abstract software entity, and accidental tasks, the representation of these abstract entities in programming languages … within space and speed constraints.”
Here’s my personal interpretation (simplification/oversimplification) of the essence vs. accident distinction in software:
- Essential problems and complexities are those that are implementation-agnostic; suspend your disbelief, and imagine you’re developing for a hypothetical perfect Turing machine with no performance limitations, and a software platform you’ve always dreamed of; many of the currently existing difficulties will go away, and those that remain (I hope you agree that it wouldn’t make all problems disappear) are essential; I also like to think of these problems and complexities as conceptual
- Accidental problems and complexities are those that stem from the limitations of the hardware and software platforms we use; (please note that ‘accidental’ in this context doesn’t mean ‘occurring by chance’); they happen to exist, but it is conceivable to at least envision an environment that wouldn’t have them; I also like to think of these problems and complexities as technical
The gist of Frederick Brooks’ essay is that the messiness, chaos and lack of rigor in the real world (i.e., the essence) all guarantee that software development would be difficult even in the absence of technical problems (i.e., the accident). He goes on to conclude that the field shall never have an equivalent of Moore’s law in hardware (i.e., order-of-magnitude increases in productivity), as any potential “miraculous breakthroughs” (technological or other) have, at best, the potential to address the accident, not the essence – hence “no silver bullet”.
Just spit it out, will you?!
Yes, finally, at long last, I am ready to enlighten you with my brilliant, magnificent idea: I believe you’re more likely to create a leaky abstraction if you attempt to abstract away from an essential (conceptual) complexity, as opposed to an accidental (technical) one.
Below I present a few examples of frequently used abstractions (mostly within the .NET platform, as that’s where I build most of my stuff these days), and discuss whether or not I consider them leaky and why.
ASP.NET Web Forms leaks, and it leaks a lot. In addition to abstracting away from a lot of technical (i.e., accidental) complexities, it tries to allow you to develop for the web while thinking you’re building for the desktop. There are conceptual (i.e., essential), differences between server-based web applications and UI programs executed on the client. I would almost call these differences philosophical, and as this technology has the ambition to abstract away from them, it is, in my opinion, bound to leak like a sieve.
ASP.NET MVC is, in a way, much less ambitious than Web Forms. It embraces the “philosophy” of the web and the HTTP protocol. It doesn’t attempt to abstract away from the essential traits of this milieu, such as statelessness or the fact that the output of the server-side code is the web page sent to the client. This framework can still make developers way more productive than they would be without it, as it allows them to stop worrying about many technical/accidental problems. However, it doesn’t alter the essence of the solution, which is why, I believe, it leaks relatively little.
Perhaps no other pair of related technologies illustrates my point more saliently than ASP.NET Web Forms and ASP.NET MVC – the former aspiring to abstract away from the essence of the problem, the latter embracing it, and limiting its ambitions to the accident.
Object-Relational Mapping frameworks, such as Entity Framework or Hibernate, have cost a lot of people, myself included, a good chunk of their sanity. (I’m tempted to say they will make you tear your hair out, but at least my baldness has nothing to do with tables and classes, really.) Once again, I believe it’s because they’re not exactly airtight, and the fact that they strive to abstract away from the essence of the problem is to blame. At the end of the day, the relational representation of data differs from the object-oriented representation, to a great extent at the conceptual level. (This is often referred to as the “object-relational impedance mismatch”.)
In the same vein, the contrast between LINQ to Objects and LINQ to Entities/SQL offers some interesting insights. I love the former, and I really like the latter. LINQ to Objects is more-or-less airtight, while LINQ to Entities/SQL suffers from the same problems as ORM frameworks. Most people who’ve used LINQ to Entities/SQL for some time have run into the situation where one of the lambdas they passed to, say, a where clause failed to execute.
The designers obviously tried to make the two flavors of LINQ “feel the same”, and I think it’s sometimes easy (too easy, perhaps) to forget which of the two (objects/entities) you’re using. Everything works the same, until it doesn’t. Have you ever used in-memory collections of objects in unit tests and a real database in your “production” code? Did you, by any chance, run into a situation where your LINQ queries worked like a charm against the in-memory objects and failed with the real stuff?
You don’t normally have to think about this, but I find it interesting to observe that LINQ to Objects and LINQ to Entities/SQL actually use different extension methods. Consider the signatures of, say, the Where method:
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
public static IQueryable<TSource> Where<TSource>(
this IQueryable<TSource> source,
Expression<Func<TSource, bool>> predicate)
Obviously, LINQ to Objects uses the first method, and LINQ to Entities/SQL the other one. In both cases, you pass the same argument – presumably a lambda – but while LINQ to Objects actually calls your function, LINQ to Entities/SQL “reverse engineers” it (that’s why the method doesn’t take a delegate as its parameter, but rather an expression – it couldn’t do this with a delegate) and converts it to a SQL snippet that it later executes against the database.
Now, obviously, there are limits to what the engine can successfully convert. Consequently, while you can use any C# code (or code written in another CLR language) with LINQ to Objects, you have to be very careful with LINQ to Entities/SQL, as the conversion of your lambda to SQL will fail, if you do anything unsupported by the provider. (Not to mention the fact that the resulting SQL is often pretty hideous and inefficient.) In other words, you as the user of LINQ to Entities/SQL have to worry about the details of its implementation. Is it just me, or does this sound like a textbook example of a leaky abstraction?
I consider this another symptom of the “impedance mismatch”. After all, LINQ has to translate from an object-oriented language, such as C#, into T-SQL, a “relational language”.
I love the functional programming aspects of C# introduced with LINQ. Having said that, I’m madly in love with those libraries and frameworks that call my lambdas (such as LINQ to Objects), and I’m simply (i.e., not madly) in love with those that “do some magic” with them (such as LINQ to Entities/SQL). I understand that often there is no way to achieve the desired effect by invoking my functions. However, I still expect a potentially leaky abstraction whenever I pass an Expression as a parameter. I don’t (necessarily) run away, but I see it as a yellow light.
Some abstractions have become so good that we sometimes forget to think of them as abstractions these days. Take device drivers, your graphics card driver, for instance. You can run into situations where an application works fine with one card and fails with another, but these are increasingly rare. A lot of this has to do with maturity – video adapters have existed for decades, and the manufacturers (of both hardware and operating systems/software) have learned from past mistakes and perfected their solutions.
To fully explain the lack of leakage, though, I believe you have to consider that the essence of the problem, in this case, is the drawing of pixels and geometric shapes, as well as the use of various effects and other related concepts, something the device driver does not abstract away from. Sure, different hardware achieves these things in different ways, but those simply represent distinct technical implementations of the same conceptual model. In other words, the drivers abstract away from the accident, not the essence.
Last, but not least, the various frameworks and language constructs designed to facilitate asynchrony and parallelism (such as the Task Parallel Library and async/await, as well as comparable features in non-.NET environments) are something I find especially interesting. I appreciate… I mean I love it that they allow me to stop worrying about the technicalities of starting new threads, dealing with callbacks, etc. At the same time, I believe that the very fact that your program potentially executes in multiple locations at the same time represents a conceptual shift from single-threaded applications; that this matters (to the programmer); and that you should always understand, at least at a high level, what’s going on under the hood.
No silver bullet… how about a silver lining?
Okay, assuming I have a point (miracles do happen), does it mean that we should only use those abstractions that abstract away from accidental/technical complexities, and shun the more “ambitious” ones that attack the essence? That is not the message I’m trying to get across. Many abstractions are extremely useful in that they can make you way more productive, even though they leak.
Above, for example, I express the conviction that ORM frameworks are leaky as a result of the fact that they’re abstracting away from an essential difference between two paradigms. Yet, I wouldn’t recommend avoiding them, as I also believe the increase in productivity they offer is hard to argue with. I would, however, suggest becoming familiar with both paradigms (object-oriented and relational) in depth before you even start.
When learning to use a new library/framework, I think you can benefit from asking: What is it that it’s abstracting away from? If it’s something essential/conceptual, as opposed to accidental/technical, maybe you should:
- Expect leaks
- Learn the underlying concepts (i.e., the essence); at least at a high level
- View the abstraction as a toolkit that allows you to build your own solution, not a readymade solution itself
The next time you encounter a leaky transaction, before condemning its developer, ask the following question: Does it leak because the people who designed it did a bad job, or is it because the underlying problem is essential/conceptual? If it’s the latter, perhaps you should alter your expectations.
It all somewhat reminds me of a sign I once saw in a restaurant. It read: If the quality of our service does not live up to your standards, please lower your standards.