If you haven’t read it before, go check out Joel’s Law of Leaky Abstractions. I bumped into some code in one of our projects last week that is a classic case of a leaky abstraction. It looked something like this:
public DataTransferObject ProcessTheMessage(Message message)
{
IMessageHandler[] handlers = createHandlersForMessage(message);
DataTransferObject dto = new DataTransferObject();
foreach (IMessageHandler handler in handlers)
{
// Check for special case
if (handler is SpecialMessageHandler)
{
// Downcast to the specific MessageHandler
SpecialMessageHandler special = (SpecialMessageHandler) handler;
// Check if the message is from our BigClient with special
// “needs” – Our code didn’t do this, but I’ve seen this
// far too frequently
if (message.ClientId = “BigClient”)
{
special.SendEmail(message);
}
else
{
special.Audit(message.Header, message.ClientId);
}
}
else
{
handler.Process(message, dto);
}
// Another exception case if the MessageHandler is Security
if (handler is SecurityMessageHandler)
{
if (!(SecurityMessageHandler).Authorized)
{
dto.Success = false;
break;
}
}
}
return dto;
}
There is a generalized array of IMessageHandler objects that take their turn processing a Message object, or at least that’s the way it started. At some point the developers needed to code a special case to short circuit the message handling chain if a specific handler was encountered, or a specific client. For whatever reasons, the developers chose to go outside the convention of the existing IMessageHandler abstraction. Once the abstraction springs a leak with one exception case, more exception cases are sure to follow rapidly. A leaky abstraction is a very clear sign that the overall structure and design of an application is succumbing to entropy and heading for its end of life as changes to the system become too risky or costly.
Preventing Leaky Abstractions
I’d say that there are a couple different causes of abstractions springing a leak. System design and architecture has to be socialized throughout the development team to be successful. I think there is a “this is the way we do this” factor that has to be in place within a development team for any abstraction to truly work. Developers that don’t understand the purpose and usage of an abstraction will go around it anytime they’re under schedule pressure – i.e. almost always.
The second big cause is abstracting too early. When you make a design decision too early you are speculating without all the facts. To some degree I think very experienced developers and architects can accurately predict what will be useful, but it’s still a gamble. When you reach too far ahead in your architecture you incur a risk of creating unnecessary infrastructure or creating an unnecessary amount of complexity. It’s taken me a couple of years now but I think I finally buy (most of the way) into the XP idea of YAGNI. Abstracting too early is acerbated by changes in requirements or priorities. The facts and understanding of the business process that underpinned your abstraction have changed and the abstracted model is busted. Wait to introduce the abstraction until it’s necessary or valuable. More on this subject below.
Architect Hubris and the Siren Call of Metadata-Driven Architectures
One of the biggest, most colossal mistakes a software architect can make is that of hubris. “Architect Hubris” is the belief that the architect(s) can design and code an application framework upfront that will reduce the remaining development to a mere exercise in configuration. I came awfully close one time with a workflow system that was almost completely configurable – via no less than 65 database tables! The “now its just configuration” moment never arrived. There was always some kind of exception case that didn’t quite fit into the abstracted model. It’s really too bad that we spent so much time getting the workflow framework upfront instead of just coding what the business asked for in specific. We delivered what we had to, but there was a lot of wasted effort in architectural infrastructure that provided no real business benefit.
Favor Composition over Inheritance
I think metadata driven design and extensibility mechanisms still has its place, but I use it differently today than I did back then. Where I extensibility I do it by abstracting at coarse-grained interfaces, not abstract classes. I then use StructureMap (shameless plug) to configure object graphs. I never assume anymore that all implementations of an interface can inherit from a single superclass or be described by an identical set of configured metadata. Read more from Erich Gamma about the “Favor Composition over Inheritance” principle.
The Last Responsible Moment
I got to participate in a panel on TDD in Austin with Mary Poppendieck last fall. She spent quite a bit of time talking about the value in making decisions at the last responsible moment. The key is to make decisions as late as you can responsibly wait because that is the point at which you have the most information on which to base the decision. In software design it means you forgo creating generalized solutions or class structures until you know that they’re justified or necessary.
Abstracting too early is problematic, but waiting too long to introduce an abstraction is almost as bad. You either end up living with duplication in the code (bad) or pay a cost in a larger refactoring to introduce an abstraction to remove the duplication. Continuous design as practiced by Agilists is not another flavor of chaotic “code n’fix.” It’s design all the time. You have to be constantly scanning your code and design and looking for code smells creeping in and opportunities to create abstraction that remove duplication or create greater efficiencies.
I’ve worked with some people who interpret Agile development to mean that you never do any design activity outside of coding. Let me say this as clearly and forcefully as possible, always be thinking ahead! Consider different abstractions, research design patterns to reduce conditional logic, and look for reoccurring patterns in the code that can be generalized. Just don’t implement anything more than you have to for the functionality at hand. Large refactorings shouldn’t sneak up on you. You should always have a list of possible improvements to your design floating around both in your head and in conversations with your teammates.