Finishing RefactoringsPosted: April 14, 2009
Technical debt, we all know, is hard to manage. To fight against it, you have to (among other things) refactor and improve your code mercilessly. But along the way, your attempts to make the code base a safer place can actually make them worse: if you add in a new way to do something without actually removing the old way, you could end up with a code base that’s more cluttered and more inconsistent, making it even harder to understand than if you’d just never tried in the first place.
To take a contrived example, imagine that you’re writing a new test that needs to create a sample hierarchy of widgets and gizmos for your application, and you realize your existing architecture could use some improvements. Over time you’ve accumulated a bunch of random helper methods that do various things, like WidgetUtil.createSampleWidgetWithOneGizmo(), but the methods therein are brittle, take too many arbitrary parameters, and are difficult to combine to create richer test data. So, you decide to refactor the test utilities using the builder pattern to make things more fully-parameterizable and chainable. Great idea, right? So you create your nifty new WidgetBuilder and GizmoBuilder classes, use them to write your tests, and as predicted they make the data setup a lot clearer and more flexible.
The question, then, is what do you do next? Do you just use them in for new tests, or do you go back and refactor the 158 existing tests that use WidgetUtil so that they use your new builder classes? And do you stop there, or do you attempt to kill off all of your old data creation methods in favor of new builder patterns for everything?
When trying to improve the code, the real work is often not in the initial improvement itself, but rather in fully replacing the old way of doing something with the new, improved way. Either option has some serious potential downside. On the one hand, forcing the change through every part of the system is usually hugely labor intensive and carries a high risk of breaking something that was already working, all for no immediate return whatsoever. On the other hand, leaving things as is just adds to the complexity of the system: now there are two ways to do X instead of just one, and every subsequent developer needs to understand both of those and know what the differences are.
So what’s the right thing to do? Well, as with most programming tasks, it comes down to a judgment call about whether to attempt to push it through the system, whether to just add the new change but not attempt to refactor further, or whether to abandon the change and just do things the old way to avoid adding complexity to the system. Here are a few questions to ask yourself:
- How localized is the change? Is it likely that most other developers will need to be aware of both ways to do things, or will only a smaller subset have to deal with it?
- How bad is the status quo? Is the improvement drastic, or merely incremental?
- How much work is the refactoring? Are there five other places where a similar pattern is present or 500?
- How likely is the refactoring to break things? Is it a fairly straightforward drop-in replacement or change, or is it something more involved that could turn out to have unanticipated interactions?
- Can automated tools do the refactoring, or is it something that has to be done by hand?
- How long is this system going to be around for? Is this something relatively throwaway (though it always seems like every program lives longer than its creators expect), or is it something you know you’ll be dealing with five or ten years from now?
- How likely is the refactoring to uncover and fix latent bugs or to otherwise clean up buggy areas in the system?
In general, in my experience most people tend to err too far on the side of not finishing things off and truly eliminating all vestiges of an old way to do something, be it in the form of a method or merely a general approach to a problem. Especially once a system gets to a certain size, the cost starts to seem prohibitive. But unfortunately, those are often exactly the times that it’s important to keep the code clean and avoid technical debt; technical debt is much more of a killer on large projects than on small ones.
So next time you come up with a new way to do something, or decide that way X is better than way Y, ask yourself if you’re stopping too early or if you should really be following the refactoring through to the end.