Thursday, May 10, 2007

Refactoring and the Law of Unintended Consequences

Refactoring has the advantage that over time it makes code easier to understand and maintain. This can contribute to the maturity of the code. It has the disadvantage that it requires changing the code and any change has the potential to reduce the maturity and stability of the product.

The so-called “Law of Unintended Consequences” states that any human action will always have at least one consequence that was not intended. The bad part of this is when the unintended consequence causes a problem. Another way to look at this is that each change will have an effect; one of the effects is the reason for making the change in the first place. All of the others can be classified in two different ways: expected or not and reasonably predictable or not. When making a change, there may be effects that are expected even though they were not the reason for making the change. As long as they are considered to be beneficial, that’s probably ok. If there is any reason for not wanting the effect, even if some observers might consider it beneficial, then it is probably best to think of the effect as non-beneficial.

The other classification is whether or not an effect can be reasonably predicted. Some effects might not be immediately obvious. As with chess, the effect of moving a queen into a capture square of a pawn is immediately obvious. Generally, it means that the queen is going to be captured. However, it might also be the case that the queen was moved there in order to guarantee checkmate in five moves. Unless the other player is looking ahead at all possibilities five moves in advance, there is a very good chance that they will miss this. Thus, the worst part of the law is unintended effects which are harmful and cannot be reasonably expected to be predicted.

Some changes have a non-linear effect on a system. That is, the change was very small – perhaps a single character was changed, but the impact was very large, such as destruction of large amounts of data. Because the amount of change often has no connection to the potential impact, it is always best to keep the amount of change to the absolute minimum required.

Even if a change could be reasonably expected to be predicted, sometimes the effort to do so is not invested. In any case, any effects that were not determined prior to incorporating the change are considered to be unforeseen consequences.

There are some strategies for minimizing the risk of unforeseen consequences. The first is to make a reasonable effort to determine effects other than just the intended one. The second is to do an impact analysis and either verify that the current requirements and code coverage are sufficiently high to mitigate the risk or to increase coverage to a reasonably high level.


John J. Wall said...

What about prioritizing the changes by the impact of their effect? Are you looking for changes that will have a non-linear positive effect as those would be the most "profitable" or would those also be the most risky?

Unknown said...

Instead of investing an effort on such complex impact analysis you should invest in Test-Drive-Development (TDD); you will not refactor your component unless you have an automatet test in place and your component (and system) pass all your test before beginning of refactoring; then after every incremental refactoring/improvement you will run the whole bunch of tests again, etc.

Damon Poole said...

The impact analysis can be as complicated or as simple as you deem appropriate, I'm just suggesting that you do it. I'm a huge fan of writing automated tests for all changes, a practice included within TDD. However, I also know that getting all of your tests to pass does not mean that you have removed all risk of unintended consequences. TDD reduces risk, but does not remove it entirely.