Improve as you go
Sometimes we come to situation where we need to perform a major change in our code base, one that is likely to affect more or less the whole system. This might be a application-wide refactoring such as extracting business logic from the controller into the model. It can be introducing in-code documentation into a undocumented system, or introducing unit tests in a legacy system (because only system’s we’ve inherited is lacking unit tests, right?).
No matter exactly what task it is, we’ll assume that completing it is something that would require many man-weeks to complete. Time that we typically do not have in the condensed schedule of a modern software development project. Thus, it can be hard to motivate spending the resources that would be required to perform such a change. All to often, this leads to a situation where change is regarded as “something that should be done, but which we don’t have time for right now”. That is the same thing as “something we will never do”. Furthermore, as we typically perform the improvement in order to make the code base easier to work with in the future, we want to prioritize the parts of the system where we spend most of our development time.
A simple rule
In situations like this, I suggest the following rather simple rule.
Every time you commit, ensure that all files in that changeset are updated to the new standard.
Thus, if your current commit touches a controller which contains business logic, you may not commit until you’ve extracted and merged it into the model. Or if your commit touches an undocumented or untested class, you may not commit until that problem has been corrected.
Rationale
The rationale behind this recommendation is twofold. First, the performing the change won’t feel like a gigantic task, as it only involves modifying a couple of classes at any time. This helps to get around the “something that should be done, but which we don’t have time for right now” problem. This helps to amortize the work over the course of the project.
Secondly, you don’t spend time on updating files on which no one is working. This connects to the assumption that the improvement is done in order to make the code base easier to work with. Thus, as long as no one is working on a file, there is no need to make the improvement there. However, as soon as someone touches that file, it will brought up to the new standard. This is really just the Last Responsible Moment principle, or YAGNI, put in a slightly different context.
As a bonus we reduce the risk that the change introduces bugs. This is because, as opposed to performing the change in one big go, only code on which the developer has just been working is updated. Thus, the developer is more familiar with the code and is in a better position to avoid introducing bugs by breaking non-obvious relationships and dependencies in the code. This advantage becomes more important the worse shape the code in question is in.
Possible drawbacks
To be fair, I should mention possible drawbacks. The major argument against this method is that the system is left in a “in between” state for an extended period of time. Depending on the type of change this could become problematic. If you’re switching from using one library to another, you might for a while be dependent on both libraries rather than any one of them. This might in turn create confusion about what library to use. Or if the system is in the middle of a major large scale refactoring, developers might in the same way be confused about the correct way to implement a feature.
I think that the best solution to this problem is to ensure that all developers are aware of what is happening. It’s really a question about communication within the team. Exactly how to solve this potential problem depends on the team. You can create a list of ongoing improvements which everybody can check when in doubt or when committing. You can emphasize collective ownership of the code and use pair programming to spread knowledge. Do whatever fits your team, as long as you consciously considers and manages the risk.
Summing it up
All in all, I think performing large scale changes in many smaller chunks is the preferred way to go. Pay off some of your technical debt every day, rather than all of it at once. After all, to pay off your whole technical debt at once, you most likely will have to take on debt of some other kind — economical, working overtime, or implementing less new business value.