Friday, 20 March 2009
A lot of the work I’ve done recently has involved substantial improvements to largish production code bases. Currently I’m working on a system of .Net apps that seems to have experienced exponential LOC growth in the few years of it’s development. Before that I was doing maintenance and upgrade work on an app that started life as some Excel spreadsheets wired together with VB, and over 10+ years (and many programmers) grew to a large Asp.Net web app with an Oracle back end. Maybe after hearing all that you’re ready to write me off as another jaded maintenance programmer but hang on, I’m going somewhere with this. These projects are very different but share some common threads. They tend to be under performing. New feature work is very expensive or nonexistent. As I spelunk the code base trying to grok these systems what I see again and again is what I have come to call unnecessary complexity.
Unnecessary complexity is a code smell, as the refactoring proponents use the term. I haven’t formed a definition but I do have some examples. A chunk of code is unnecessarily complex when it is making more method invocations or using more logic constructs than are necessary to complete it’s intended purpose. A system is unnecessarily complex when it has half implemented features that are not being further developed. A component is unnecessarily complex when it has features that are thought to be working and even in active use, yet actually contain a bug or bugs that invalidates the feature’s implementation. A prime example of this is buggy code wrapped in overzealous exception suppressing try/catch blocks.
How does code get unnecessarily complex? One reason I have found is developer ignorance of the language/platform and it’s associated APIs. This is most pronounced when a new development project is undertaken by developers who are also new to the chosen language and/or platform, even if they are otherwise seasoned developers. Another reason is code rot introduced in periods of maintenance, particularly when there has long been pressure to complete changes quickly.
So often I see that the two problems I mentioned earlier, under-performance and slow feature development, can be solved with a two step process. First reduce the complexity in a series of micro steps. Keep going, until the point you’ve transformed it into simple understandable code. Eventually it becomes obvious where to target your feature or performance changes. I know this sounds like plain ole refactoring – improving the design of existing code – but it is subtly different. Is modifying try/catch blocks or reducing if/else nesting really design? Maybe it’s design in the small.
I’ll be doing a series of posts on specific examples of unnecessary complexity. I don’t want to create my own little daily wtf, but rather engage in a productive analysis of examples inspired by my daily work, both past and present. I hope to eventually identify some psuedo-formal patterns and anti-patterns inherent in unnecessarily complex code, but certainly will suggest my own improvements, best practices, and complexity reduction transformations as I go. Most examples will be at the micro level: statements, blocks, and functions.