Growing Through Refactoring
These are interesting times for people who care about refactoring. The 2nd edition of Martin Fowler’s book Refactoring has just been released. As if that weren’t enough, Kent Beck recently wrote up a workflow called test && commit || revert, which can be seen as a new alternative to Test-Driven Development.
It might not be obvious why these two events are related, but if you take time to click through and read Kent’s write-up on the practice (and try it out), one of the things that you’ll notice is that it is very refactoring-intensive. It points toward what I think is the most important aspect of refactoring: practicing refactoring doesn’t just improve our code, it helps us become better at reasoning about our code.
How could that be?
Making our code better doesn’t seem like it would have the effect of making us better, but there is a nice causal link. It's just not very obvious.
One of the most interesting ideas that I’ve ever encountered is shared space urban design. In shared space environments, planners remove traffic signals, curbs, and street lanes, paradoxically making roadways and pedestrian areas safer. The key to this is uncertainty. You can’t be cavalier when you drive or walk, you have to slow down and be careful. Care leads to safety.
When I learned how to program, I noticed a variant of this. I had taught myself C as a first programming language. Today we malign C quite a bit, but back then I noticed that learning C first gave me an advantage. When I later took an introductory programming course in Pascal, I was shocked when – late in the term – I saw other students getting runtime errors when they indexed arrays out of bounds. I’d been doing the same exercises in the same language for a couple of months, but I’d never seen that error. In C, when you index out of bounds using pointers, you either get a system crash, or worse: just silent corruption. After chasing the latter down a few times, you just learn not to index out of bounds during a traversal.
It’s tempting to build this up into a case against type checking. I don’t want to do that. Often safety is so important that you want to have guarantees built into the code. But there’s something about doing the mental work to just know that what you’ve done is correct.
Let’s go back to refactoring. Whenever you make a small change to your code and you are about to run your tests, ask yourself first whether they are going to fail. Actually, no. As you make any change, know with every edit whether that edit changes behavior. For instance, as you type each character of an identifier your code moves into a broken state because it simply won’t compile until you’ve finished typing it completely. What are the next keystrokes that allow your code to compile with all tests passing?
Here’s the thing. We should know that our tests will pass before we run them. We should be right most of the time. The times when we are wrong? Well, we should really stop then and figure out what we were missing. Where did our mental model of the behavior of the code diverge from the actual behavior of the code? That’s the space where bugs happen.
The great news is that the more you do this, the better you become at quickly understanding the steps you can take to change structure. You build the repertoire of things you can do freely with safety and those things become automatic for you. By testing ourselves, we grow in skill.
These are good times for refactoring. If you get a chance, look into Arlo Belshee’s work with provable refactorings, Llewelyn Falco’s practice of using prefixed commit messages, and Kent Beck’s vision of a scaling environment called Limbo, where all changes are small and knowable. These are all related ideas that grow us and our code.