Skip to main content

Code Maintenance Requires More Than Testing

Writing and running automated tests can go a long way to help maintain the quality and reliability of a code base. Tests help ensure the code you've written executes the way you expect it to. However, even though automated tests are a great tool for helping to ensure quality over the years in which people will contribute to the code, they are not sufficient in and of themselves. It may be inevitable that all software products will reach a point of obsolescence no matter how much work is done to keep them current.

The inherent problem is that if we view tests as constraints upon what the software system under test can be, then as we add more and more tests over the years, we will have more and more constraints on the shape our software is allowed to take. It then seems we must reach an inevitable point where we can no longer change our software system without violating a constraint, i.e. causing a test to fail. In more practical terms, we'll more likely first reach a situation where it requires too much effort to be worthwhile to make any substantial changes while still satisfying all of the constraints. There may not be a way to avoid this situation in the long run, but if we want our software to have a long and happy life, we can take steps to prolong it until it reaches this unhappy fate.

One of the best ways to extend the life of your software is to take make use of one of the other advantages that tests can provide: the ability to aggressively refactor code with some confidence that the refactoring has not caused regressions in functionality or reliability. If you have a good, thorough set of automated tests, they will inform you when changes you make to existing code introduce new bugs or break or remove existing features. This gives you the ability and confidence to rewrite existing code whenever you see an opportunity to improve it.

Once we've established the ability to rewrite existing code, the question remains why would you want to do so. If it ain't broke, don't fix it, right? You can certainly make an argument for leaving functioning code alone, particularly when you have so many other tasks to tackle. Especially when those other tasks involve adding new features that will provide real value to the software and its users.

Refactoring provides value as well though. It enhances the long-term maintainability of a software product. It paves the way for new features to be added in the future more easily than they otherwise might have been. Rewriting functioning code is a long-term investment in your software. Like all long-term investments, you pay a price up front, but you don't reap any rewards from it until later. That always makes it a harder sell, but like many long-term investments, refactoring can yield rewards several times greater in proportion to the initial effort.

One of the best ways I can illustrate this is with a case I encountered. We had some thread and database session management code in a code base I worked on. There was originally a very grand design for that code, but it never ended up working the way it was intended. Over time, it was modified, and its functionality curtailed, so that its ultimate behavior was relatively basic. However, the code itself was still very complicated, and very convoluted due to the evolution.

The code worked for the most part. However, on occasion, some change somewhere or deployment into a different environment would trigger a hidden bug. It was extremely challenging to debug and fix, as the person who had written the code originally had left the company, and no one at the company fully understood how the code worked. However, as it very rarely needed to be fixed, and as we all thought it would take a lot of effort to replace, we left it alone.

Finally, I had a little time and felt inspired, so I took a stab and rewriting it with a focus on making it as simple as possible. I boiled it down to the most basic operations the code needed to execute. At the end of three days, I had written a fully-functional replacement for the code that had plagued us with its inscrutability for so long. Not only did it fully meet the functional requirements, it proved to be over 6 times faster in execution.

The end result on the code size was that one implementation file was completely eliminated, another one was reduced by more than 90% in size and a third was cut by more than half. Thus, we ended up with a faster implementation that performed the same function with substantially less code, and not only was it less code, it was far clearer and cleaner code. This last part was critical because it meant that when someone needed to understand or modify the code later on, it would be much, much easier. We went from no one understanding this part of the software to everyone understanding, or at least being capable of understanding it.

This story illustrates what I think is the most important component of good code maintenance: removing old, unnecessary code. I particularly love the words of the writer Antoine de Saint-Exupery:
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
After a certain point in a software project's lifecycle, as it continues to age, the number of developers working on it will only trend in one direction: downward. While there is no hard and fast limit, conceptually it makes sense that there is some cap to the ratio of lines of code to software developers in order for a project to be well-maintained. The problem is that while the number of developers working on a project starts to dwindle, the size of the code base tends to increase.

While it may be difficult, if not impossible, on many projects to effectively reduce or even maintain the number of lines of code in a project over time, the least we can do is curtail the increase as much as possible. More lines of code equals more complexity and more complexity equals greater maintenance effort. Thus, eliminating lines of code whenever and wherever possible without causing regressions or loss of important functionality can go a long way in extending the maintainability of a software project.

Code is not sacred, it will evolve and change whether you want it to or not. If you don't exercise some control over the way it evolves, it will grow wild and tangled and become convoluted and hard to work with, comprehend and maintain. If you are vigilant in trimming back unnecessary branches, you can keep it neat, orderly and maintainable for many years to come.

Comments

Popular posts from this blog

Books That Have Influenced Me and Why

A mantra that I often repeat to myself is, "Don't abandon the behaviors and habits that made you successful." I believe this trap is actually much easier to fall into than most people realize. You can sometimes observe it in the context of a professional sporting event such as American football. One team might dominate the game, playing exceptionally well for the first three quarters. Then, as they sit with a comfortable lead, you see a shift in their strategy. They start to play more conservatively, running the ball more often than they had. Their defense shifts to a "prevent" formation, designed to emphasize stopping any big plays by the other team while putting less pressure on the short game. The leading team often looks awkward in this mode. They have switched their perspective from that of pursuing victory to that of avoiding defeat. They have stopped executing in the way that gained them the lead in the first place. I have seen more than one game ult

Notions of Debugging

Bugs can be really evasive and produce some surprising, even "impossible" behaviors. Part of a software developer's job is to relentlessly hunt them down and destroy them. However, often at least half the battle is finding them to begin with. The best debuggers I have seen rely a lot on experience, both experience with the application and experience with debugging in general. The former is situation-specific, and there many not be any good ways to advance in that regard other than practice. However, we can extract some information pertaining to good debugging practices in general. Dig for Clues Using All Available Resources The first step in debugging an issue is ideally to reproduce the problem. This isn't always possible, but if you want any significant level of confidence that you have fixed a bug, you need to observe the broken behavior and understand how to trigger it from a user's perspective. Sometimes we're lucky, and a tester has taken the time to