- > Company
- > Company Blog
- > Blog Detail
Non-intrusive refactoring
12.01.2010 11:17 ( 0 comments )By Mindaugas Žakšauskas
Have you ever been in a situation when it is no longer possible to put off your technical debt? We have.
It's one of those moments when a very important customer has just discovered a bug, requiring you to release a hotfix. This requires you to add a conditional into your existing little fluffy method just to handle one special case differently. And, looking at the code, you suddenly realize the contract of this method just can't be changed because the rest of the world would fall apart.
Or one of those moments when you realize you just can't carry on adding yet another little fluffy method to your 3000+ line class. A class which has been around for quite some time and widely used by dozens of your customers, yet someone is demanding you to add just a few more improvements.
We've all been there. Even the world's biggest organizations, packed with the brightest heads, are victims of their own legacy.
You may say, why can't they just fix it! Not so easy. Contrary to, for example, the construction industry we cannot close the road, repair a hole and let cars go again. Because cars in the IT industry are so well integrated into the road's surface, that they require these holes to be here - sometimes for ever. Patching the hole might make sense for new cars but older ones would just stop functioning. And this is what we call the problem of backwards compatibility.
So, backwards compatibility is quite important. Some people even claim you can lose entire market dominance just because you fail to grasp it. And you know what? These people are right.
A business' reluctance to change a dependent component is directly proportional to the investment made in it. Ever wondered how much it costs to migrate from VB6 to VB.NET?
Getting backwards compatibility done properly is very, very hard, if not impossible. It's like getting your new car audio mp3 player play cassettes. Therefore, the first rule is: get your design and API right as early as possible as you might not get another chance. This rule reminds me of a magic trick my si-fu once taught me: if somebody attacks you with a knife, run as fast as you can and never fight with your bare hands. To sum it up: the odds are against you. And by they way, getting the design right from the beginning is hard, too. Discussing the subtleties of this skill are well beyond this post, and I am sure there are plenty of books and other resources available online.
As you have read up to this point, chances are you couldn't escape the inevitable and have been forced to make tough decisions. Very much similar to a knife assault, try to avoid the conflict at any price.
Let's say somebody had spotted a problem in your code, requiring you to add
if (someCondition) { // handles some special case but might result // unexpected behaviour for other callers
}
conditional to
public class SomeClass { public SomeType someMethod(int one, char two) { ...
}
}
At the very moment of fixing the problem, the condition block looks so natural you can't even realize the possible negative side effects to all of your users. Don't be too hasty! What if somebody relied on flaky behaviour and made their own workarounds? What if your method (erroneously) returned null and people were doing all these null checks afterwards?
It is easy to say that all of these difficult questions were pointless if the module had decent unit tests. This makes a lot of sense, but only to some point. Once a mistake sneaked into the main code, unit tests will likely to have their own holes, too. Even if you use almighty trendy TDD and write your tests first, it can still fail to guarantee that all aspects of a method are covered.
In this situation, the safest bet is to refactor the code like this:
public class SomeClass { /**
* ...
* @deprecated use {@link #someMethod(int, char, boolean)}, this method * does not properly handle [...your special case...]
*/
@Deprecated
public SomeType someMethod(int one, char two) { return someMethod(one, two, false);
}
public SomeType someMethod(int one, char two, boolean someCondition) {
...
if (someCondition) { // handles some special case but might result
// unexpected behaviour for other callers
}
...
}
}
What has just happened? We have introduced another method with this extra parameter and deprecated the old one. By adding another method we:
- made it clear that our previous method was not handling some cases well enough
- provided a safety net for existing callers, with a warning of backwards incompatibility in any future releases
Note the emphasis on safety. This approach is far from a gracious solution as it introduces an extra layer of mess in your public API. So if we wanted pure clarity, we would have picked to remove the deprecated method immediately. But this illustrates a reasonable safety-first trade-off.
The above example is, of course, very dull and trivial. Real world scenarios can be much more complex and sometimes impossible. Take a look at how JSR-310 aims to replace poorly designed java.util.Date and java.util.Calendar:

The picture above (page 67 of this Javapolis'2007 talk) is a nice UML diagram where it is obvious how old deprecated classes fit into the new structure, yet retaining full backwards compatibility.
Another interesting clash which had sparked lots of posts in the blogosphere was Microsoft's decision to switch to standard compliance mode instead of defaulting to backwards compatibility. This decision was likely caused by the superiority of competitors' browsers at that time and the fact that IE was losing its market share - and losing it fast.
But let's get back to more tips of non-intrusive refactoring.
Never rely too much on varargs - neither when designing nor when refactoring. It might look safe (read: backwards compatible) to convert your last method argument to vararg but this is nothing more than a mere hack. Varargs are slippery, and certainly never designed to help to handle backwards compatibility. As Josh Bloch says, "just because you can it doesn't mean you should".
Always document why a certain routine has become deprecated and what should be used instead. If possible, add when it will be removed. Robust projects even have their backwards compatibility policies. Define those policies and stick to them.
But that's not all: depending on your release schedule, make sure you have mentioned backwards incompatibilities in your release notes document as this makes customers less angry when they bounce back to you after spending days on upgrading.
And one more important thing: never remove any part of the API in a minor release. I am sure your company's marketing department has its own ideas about what makes a major release. Anyway, the general rule is that marketing people tend to call releases major more often than you would like to hence deprecated APIs have bigger chances to stay around for longer.
What is your story on safe refactoring? Do you have any tips to share? Let us know.
Special thanks to Albert and Sindri for contributing some ideas to this article.

Comments