A Refactoring Workout: Relentlessly Green

Of course, they have an advantage over us, since they have thousands of exercises and progressions that they can use to improve any aspect of their craft.

We, on the other hand, are making it up as we go along.

Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure. —Martin Fowler

How do you get better at refactoring?

Well, you read the refactoring books and do more refactoring. Basically.

That’s not quite as helpful as, say “practice this independence drill using a metronome until you can play it faultlessly for 120 seconds. Start at 30 beats per minute (BPM) and work the drill in increments of 5 BPM until you can consistently play it at 180 beats per minute.”.

Notice the word drill in there. A drill is a very focused form of practice. It targets a technique to the point of warping it, making it possible to repeat a particular aspect of the technique over and over, notifying you immediately if you’re doing it wrong.

The immediate feedback is crucial in a drill, because if you’re practicing incorrectly, the repetition will solidify the flaws making it even more difficult to perform correctly later.

Refactoring is Not Rehacktoring

Too often we take leaps, not steps, while refactoring. However, while we’re hacking away at something which we just know in our gut is going to work (eventually), the tests are failing.

That is, if we’re even running them.

One of the challenges of refactoring is succession—how to slice the work of a refactoring into safe steps and how to order those steps. —Kent Beck

A safe step is one which does not cause the tests to fail.

This refactoring drill focuses on succession, repeating the act of choosing a tiny, safe step over and over again, with an automated test providing continuous and immediate feedback.

Refactoring Under Green

The starting point of the exercise is a simple test with a trivial implementation that is passing. The goal is to refactor the code in tiny, safe steps until it has become a generalized solution.

The size of a step is limited by what your editor can revert with a single undo.

There are two rules:

Run the test after every single change.

If the test fails, it should take exactly one undo to get back to green.

The test suite is minimal, asserting that the default string representation of a Checkerboard instance returns a grid with alternating black (B) and white (W) squares. The board in question is as small as possible, while still defining a checkered pattern.

When the refactoring is complete, it should be possible to call Checkerboard.new with any size and get a properly formatted checkerboard.

It’s tempting to add another failing test at this point, perhaps for a 3×3 board, in order to triangulate towards better design. But this isn’t an exercise in Test-Driven Development.

In this drill, the design will be driven by conscious choices about the next safe step, not by a failing test. If the step turns out not to be safe, hit undo once and you should be back to green.

Where to Begin?

The current algorithm has some issues.

def to_s
"B W\nW B\n"
end

For one, it doesn’t scale. Also, it mixes data and presentation.

Ideally, the grid would be built up separately, and the to_s method would manipulate the grid to provide a string representation. The newlines make it clear that there are two of something here. Two rows. That smells like an array.

How do you go from a string to an array without failing somewhere along the way?

How do you disconnect a hospital from the electrical grid without killing a few patients?

Redundancy.

Put something in place that will let you fail-over safely.

The Redundancy Ploy

Write a completely new implementation, and stick it right before the old implementation.

def to_s
["B W\n", "W B\n"].join
"B W\nW B\n"
end

Run the test.

The new implementation hasn’t been vetted yet, but it has been executed, which provides a syntax check.

Delete the original implementation, and run the test again.

def to_s
["B W\n", "W B\n"].join
end

If it had failed, a single undo would put the working code back.

Use the same technique to move the newline out of the hard-coded rows.

def to_s
["B W", "W B"].map {|row| row + "\n"}.join
end

We’re so used to thinking of duplication as the enemy. However, in refactoring, duplication is a short-term, low-cost investment that pays excellent returns.

The Setup-and-Swap Ploy

It’s difficult to get at the individual cells. It would be easier if they were in an array of their own. The setup-and-swap ploy adds all the code necessary to be able to make the final change in one small step.

This has moved the whitespace away from the individual cells, but we can do better. The join belongs within the loop.

This is particularly tricky, since we have two joins that need to be collapsed into a single spot. If we move one, the the test will fail. If we delete both, the test will fail. If we stick a join on the row within the loop first, the test will fail.

We could use the redundancy ploy, introducing a rows2 variable, but there’s an easier way.

This algorithm will work for a checkerboard of any size, provided that we loop enough times. We’ve been passing the argument that we need to the new Checkerboard instance all along. Use the setup-and-swap ploy to make that data available to the rest of the instance, and then replace the magic numbers with calls to size.

This is not a final solution. The to_s method is afflicted with a number of code smells. Perhaps there should be a Cell object which has a to_s method that decides what the string representation of black or white looks like. All of these changes can be made using safe steps.

Closing Thoughts

Wait. Really? You would monkey-patch String just so that you can make a change in one step rather than two?

Uh, no. Not really. That would be ridiculous.

These ploys are hyper-focused exaggerations. Variations on them can be useful in the real world, but the biggest outcome of performing this drill a few times is a shift in perspective.

Suddenly you will find yourself finding sneaky, creative ways of making a change without having to take the site down or having to deal with long-lived branches.

You may also find that your sense of scale gets adjusted. Small steps tend to be smaller than we think.

And lastly, you might discover that your tolerance for grotesque code increases.