Interface Design and the Law of Leaky Abstractions

Programmers are always trying to make things simpler, usually by making them more complex.  Interface too complicated?  Need a bit of extra functionality?  Want to work closer to the problem?  Add a layer of abstraction to the code.  As most programmers know, this is, ironically, usually the right solution.  Abstraction is critical to breaking a problem down into managable chunks. Of course, as a friend of mine once said, in programming every problem can be solved by adding another layer of abstraction, except for the problem of too much abstraction.

Joel Spolsky once made an important observation about abstractions, which he dubbed the Law of Leaky Abstractions.  As he explains, an abstraction has leaked when a user of the abstraction needs information, data, or knowledge of the underlying implementation or interface.  And Spolsky's observation is an inconveniently true one.

The Law of Leaky Abstractions: Every non-trivial abstraction leaks.

So what does this mean if you're designing an interface?  You've got some kind of lower-level functionality that you're exposing, so that consumers for your interface can use the functionality of your much simpler interface without regard to the ugly details of the underlying interface.  Your new abstraction is so good that nobody will want to use the underlying stuff anymore.  In fact, you're so confident in your new interface that you don't need to expose the old one, right?  Wrong.  Your abstraction will leak.  No matter how careful you are in creating the abstraction, there will be a scenario where users of your interface will need knowledge of the underlying implementation or access to the underlying functionality.  Your abstraction didn't fail.  It just leaked.  But if you've hidden the underlying functionality, then you've just disabled every scenario in which a leak occurred.

Corollary to the Law of Leaky Abstractions: An abstraction should not hide or disable that which it abstracts.

This applies to wizards, COM interfaces, API functions, communication protocols, and all other abstractions.  I worked on an IRC bot a while back which could take third-party plugins.  We abstracted away the IRC protocol into an object-oriented interface that would track channel state information locally and present modes and information through accessors in the interface.  We implemented channel.IsModerated to wrap mode 'm', channel.IsTopicLocked to wrap mode 't', and so on with all of the standard modes.  We couldn't think of any reason to need the IRC protocol directly, so we didn't expose it.

One network, however, had implemented a custom mode 'U' to block URLs in channel.  Obviously we hadn't include an accessor to this mode (we weren't psychic), so our abstraction leaked, and our bot's plugins couldn't use this new mode.  The solution was not to add a channel.IsBlockingURLs accessor to our class.  That would lead to interface bloat (and a waste of our time) as we have to add a new accessor for each custom one-off mode or protocol extension.  No, the solution was to enable plugins to program through our interface and write directly to the protocol, which we did in our next release.

The Law of Leaky Abstractions says that you can never completely abstract away the whole protocol/interface/function/etc.  So don't try.  Abstractions are still very useful.  They make things easier for the 80% case and improve efficiency.  But when designing your abstraction, be careful that you don't also disable the 20% case in the process.