Secure, don’t fix
When you work in a code base with lots of technical debt, making changes has become hard. Past choices turn out not to fit in the current world, and therefore the code resists change. The complexity of the code base grows.
It is therefore important to pay off the debt incrementally, in order to not reduce future development even more. Technical debt cannot be avoided up-front, but must be resolved afterwards.
Read more: Secure, don’t fixIn my experience, most of the debt comes from design choices that turn out to be not optimal after some time. Reworking these design choices to make future development faster is hard work.
I have been working in a legacy code base and I did a lot to remediate design flaws. I experienced multiple times that performing a partial refactoring won’t actually improve the code base. The old mechanism will be used even in new code because people are used to it. Moreover, there’s the interference between the old way and new way as long as they coexist. This is basically what happens with lava layers.
How to secure refactoring steps
The only way to avoid such situations is to make complete changes, where writing code in the old way is not possible anymore. For example: if you have two competing libraries and you want to phase out one, then it is not sufficient to replace half of its usages. You can only get away by removing the obsolete library completely. Only then do you have a the benefits of having a single way of working, and not having to learn two libraries instead of one.
But it might turn out that replacing all occurrences is a too large task to do in a single refactoring step. The change would just take too long, and introduce too much risk. If that is the case, it helps to look for ways to perform the change in a set of smaller increments. This requires creativity and intimate knowledge about your code base.
For example: you could put the library-to-be-removed behind a facade as a first step, making it unavailable for the rest of the code base. Only the facade itself can use the library and everything else must use the facade. Then, as a next step, the facade implementation can be replaced by the other library. Each of these steps is a complete change that makes some aspect of the old way impossible. This allows it to be performed in parallel with other development.
Making secure bug fixes
When fixing a bug, I always make a serious effort to look further to see if I can eliminate a class of bugs instead of just this one. This takes much more effort than a quick fix, but the upside is that the same kind of bug won’t happen anymore.
Fixing classes of bugs almost always requires a design change. The other day, I ran into a good example. We had a piece of software that had to report to an external service when certain operations complete. The bug was that sometimes this report was not sent. When viewing in the code, it turned out that there were at least five places where the operation was marked complete, and in only four of them the report was sent. The quick fix would be to update the fifth place to also send the report. However, since there are already five sites, it is clear that sooner or later a sixth location will pop up. And probably the report will be forgotten by then, so eventually a new bug will pop up. The only way to actually secure the change was to make a single location responsible for both completing the operation and sending the report, so that they are guaranteed to happen together.
Securing new development
For new development, everything seems easy. Just follow the SOLID principles and you’re set. Except that this is harder than you think. You see, the real world might just be a tad different from what you think up front. This might mean that you end up with the wrong design patterns applied, things separated that need to be combined, or things combined that must be separated. So you still have to refactor as soon as you gain new insights.
However, it clearly helps to think about the design before making a change. Introduce the right abstractions. Think about the client of the code before thinking about the implementation. And don’t hesitate to refactor when you see that the initial design does not fit well.