Blind Refactoring, Guided by Tests

coding
Posted on: 2013-02-08

"How can you write code if you don't understand what you're doing?", my wife asked me. It was a reasonable question, and I had a hard time answering it. But I had just discovered, to my surprise, that I could.

I'd been working on a very important piece of code for my company. It dealt with some complex financial issues, and it was difficult for me to grasp.

Because I wanted to be very sure my code would be correct, I started by working through every possible circumstance under which the code could be called. With help from a coworker, I drew a branching tree of possibilities until I was satisfied that we had covered them all.

Next, I wrote tests to cover all of those scenarios. Together, we were able to answer, in each scenario, what the code should do. With these tests, though I couldn't see the big picture, I had a target to hit: make these tests pass, one at a time.

So I started coding, blindly. I'd write a bit of code, get a test passing, and move to the next test. Since I couldn't understand the overall problem, I just wrote code that parallelled the structure of my tests: a giant pile of conditionals.

Eventually, the tests all passed. But egads, the code! It made my eyes bleed! Ignoring the method and variable names, the structure was something like this:

if thing 

  if thing > thing

    if thing > 0
      self.thing += thing

    elsif thing < 0
      self.thing -= thing
    end

  elsif thing == thing

    if thing > 0
      self.thing += thing

    elsif thing < 0
      self.thing += thing
    end

  elsif thing < thing

    if thing == 0

      if thing > 0
        self.thing += thing

      elsif thing < 0
        if thing.abs > thing
          self.thing += thing
          self.thing = thing
          self.thing = 0
        else
          self.thing += thing
        end
      end

    else
      if thing > 0
        self.thing += thing
      else
        self.thing += thing
      end

    end

  end
else
  self.thing += thing
end

This was not acceptable. I wanted my code not just to solve the problem, but to explain it to the reader -- and that included me! There were a few comments, but they didn't clarify much, because I didn't understand the code myself.

All I knew was that, after carefully reasoning through every use case and writing a test for each one, I had written code that was correct. My test suite passed.

Now I wanted to improve the code, so I started refactoring. I was still walking blindly, but I had the tests to guide me. I made one tiny change at a time, backing up if the tests ever failed. Ever so slowly, the code shrank: fewer conditionals, less duplication.

More than once, I thought I saw where I was going and tried to skip to the answer, but each time, I broke the tests, so I went back to groping.

Finally, I came to a point where the code was so short that I actually could reason through it. And something looked fishy: there was a conditional that didn't make sense. The tests failed if I removed it, but now that I could see the business logic behind it, it seemed wrong.

So I went back to the whiteboard and graphed that part of the logic again. I stared, I reasoned, and I talked with my colleague again. Sure enough, one of my tests was wrong! I fixed it, and I finished my refactoring.

When I was finished, I had something like this:

if thing.to_i == 0
  self.thing += thing

  do_something if thing < 0

else
  self.thing += thing
end

Finally, I could see what was really happening! Looking at this code, I could articulate the principles that drove all the test cases we had written. I could understand what was happening well enough to give the methods and variables very clear names, and I could explain why the code did what it did in the comments.

If I were smarter, I might have gotten there by reasoning. But I didn't. I got there by blind refactoring.

How can you write code if you don't understand what you're doing? With a thorough test suite, you can. Very, very, slowly.