This blog post is part of the mini series about Framework Design Principles.
When writing a framework or library, we have to simplify things and create abstractions. But: With every new concept or abstraction, our users have to learn more. They can not simply forget about the underlying stuff. And they can not simply ignore other concepts. This does not have to be a problem, we just have to think about it.
The law of leaky abstractions
Abstractions make working on large code bases possible: We don’t want to deal with low level stuff. In fact, we can not deal with it - A large project would not be feasible if we had to write it in assembler. Or if we had to take care about disk I/O or scheduling. So, we need libraries that create new abstractions and/or simplify things for us.
But, as Joel Spolsky has said:
This means that the abstraction, in some cases, does not work anymore. The user has to “dig deeper”. The underlying concepts “leak” through the abstraction. For example, Apache Wicket tries to hide the fact that we are dealing with HTTP, a stateless protocol. Writing a web application with Wicket feels a little bit like writing a desktop application: There is a flow of events, components pass data to callback methods, etc. The communication over HTTP is not an issue, ever.
Except when it is. Every wicket component has a strange method:
To really understand what this method does and why it is needed, one has to understand how AJAX works and that we deal with a stateless protocol here. This method is only needed when a hidden component should be manipulated with an AJAX call. Hidden components are normally not written to the output (HTML) at all. The AJAX request is a new, stateless HTTP request, and the only thing at the client side that can be manipulated is the HTML from the last (stateless) HTTP request. Because of this, when an AJAX result should manipulate the hidden element, there has to be a HTML element which can be replaced with the AJAX result - the “Markup Placeholder Tag”.
Moq - .NET
Another example is Moq, a neat mocking framework for .NET. Moq is great: It uses lambda expressions to configure mocks - They are checked by the compiler at compile time and refactoring safe. Configuring the return value of a method looks like this:
mock.Setup( foo =>foo.DoSomething(It.IsAny
()) ).Returns(true); </pre> This means: If a method called
DoSomethingis called on a given mock object ("foo") with a string parameter (
()</code> - the value of the string doesn't matter), then return true. There is some magic going on behind the scenes to make this work, but normally you don't have to care about that. Except when the magic stops working: Like, when you want to mock a protected method. Then, suddenly, you have to write different code:using Moq.Protected() ... mock.Protected() .Setup ("Execute") .Returns(5); </pre> If you want to truly understand why this difference exists you'd have to understand how the magic works in the first place and why this is not possible with protected methods. The easier way is: Just use the second code snippet for protected methods. This probably works for most of the users of this framework, but when there are unexpected problems, they still have to dig deeper.
What do doSo, all abstractions we create will be leaky. But we have to create them, otherwise developing large system might not be possible anymore some time in the future. For every new concept our users potentially have to deal with 2 concepts: The new one, and the old one. But inventing and creating new concepts is part of the essence of our job as programmers. Are these "leaks" a bad thing? Not necessarily, I think. We just have to be aware of them when designing a framework. And we have to make sure that they happen at defined places, and in defined situations. We have to know the limits of our abstractions and concepts. And we have to look at our framework or library from the perspective of our potential users. Both are a good idea anyway. Anyway, we should be really careful with creating new abstractions in the first place. We are always at risk of causing more trouble for our users than what they get as benefit: They have to remember more concepts and their programs become potentially harder to read, write and debug.