One of my coworkers had a fantastic comment: “If you’re afraid to refactor a code base because you’re afraid of instability, your code is probably already full of bugs.”
I often speak with developers who fear to modify their code beyond the immediate feature or bug fix. Many say, “If it’s not broken, don’t touch it.”
I find that to be short-sighted thinking. Uncle Bob believes in the Boy Scout rule: Always leave the code behind in a better state than you found it. Martin Fowler believes in Opportunistic Refactoring.
It’s not as difficult as you might think. And as Martin Fowler says, “The whole point of refactoring is that it makes the code base easier to work with, thus allowing the team to add value more quickly.”
The ideal to strive for is a well-crafted, well-tested code base, in which you are not afraid to make small (and sometimes large) changes while also retaining confidence that your code works how you expect it to. The cold, dark reality is that we rarely work within this ideal world. We often find ourselves dealing with messy, legacy code bases that are… shall we say… delicate. And delicious!
Is it Tested?
When you find yourself in this dark pit of despair, first find out: Are there any tests for this code? And if there isn’t? Write one. Just one. It’s a happy start. It doesn’t have to do anything fancy. Just aim to exercise as much code in one pass as you can.
Does it already have tests? Fantastic! Now, look for the biggest chunk of untested code you can find and add one more. Code coverage analysis tools are great at uncovering this sort of thing.
What kind of test should you write?
- Does your code have many external dependencies*? You’ll probably need to rely on a functional test for a slice of behavior before you perform your refactoring.
- Does your code have more distinct, singular responsibilities? A simple unit test to verify that responsibility will likely be easy to use here.
Doing it Right: Making the Change
While making your code a better, happier place, there is a way to do it wrong. Don’t try to refactor and add functionality at the same time. You’re asking for a world of trouble – like Chuck Norris in a bar fight. If something goes wrong with your change, it will be difficult to unwind whether your refactor was the cause or the fault lies with your new functionality. You don’t want Chuck Norris messing with your code!
These code patterns can serve as a guide:
Code duplication – This is the easiest one and you know it already. Simply move the behavior to a new function, and call it from each duplicate location.
Abstractions – Is there an algorithm that you can generalize? Pull out the special-purpose dependencies to isolate the algorithm.
External dependencies – This takes two forms:
- The first looks similar to abstractions. Separate dependencies from the logic that consumes them and move that logic to a new function. It may only be used from the original function, but your new logic function will likely take a form that is very easy to independently test.
- The second is more difficult. Sometimes, you can’t see much of the logic you’re trying to unwind. In these cases, it’s often easiest to take the external dependencies and quarantine them. Think of these external dependencies like ebola – you don’t want to get them any closer to your beautiful logic than necessary! Move each distinct dependency into a separate proxy function, or move them all to a separate class you can call a “data provider”. Before long, you’ll see the underlying logic fall out.
Self-Cleaning… and Testable!
If you apply this methodology, you should find yourself with a self-cleaning code base. You’ll see a common separation appear between data and behavior as your dependencies separate from your logic. And, as simpler common logic becomes more abstract, you’ll find yourself building more standard libraries for consumption.
As your logical behaviors separate from their dependencies, you’ll find them very easy to unit test, without having to execute lengthy code paths.
As your data dependencies separate from your logic, you’ll find them very easy to functionally test, without requiring heavy lifting and class configuration.
*External dependency: The easiest way to define an external dependency is to find that code that you don’t fully control every aspect of from the local code base (File I/O? Network resources?) or that code that is non-deterministic within the code (Random number generation? Something else that throws exceptions?). When in doubt, isolate it!