Simpler Behaviors are Easier to Specify Accurately

Yesterday I told you to debug early and often, but this write-a-bit-run-a-bit strategy doesn't pay off as well if you're trying to code up long, rambling functions or entire features all at once. Remember, if your job is to correctly and precisely specify the behaviors of your robot armies, that job is made much easier if each individual behavior is kept simple and focused. This is one of the keys to writing code that is easily debuggable. And code that is easily debuggable is almost always more stable and reliable.

Put another way, complex behaviors should be composed of simple, well-tested behaviors which you already have confidence in.

Imagine you're trying to write a function to return the score of a bowling game. The function takes an array of numbers, 1 through 10, representing the number of pins knocked down with each throw.

If you already know how to score a bowling game, you can skip this paragraph:

A game is composed of ten rounds, called frames. In each frame, the bowler has two chances to knock down all ten pins. If they knock down less than ten pins, they get points for that frame equal to the number of pins they knocked down. If they knock down all ten pins in two rolls (a "spare"), that frame gets ten points plus whatever they roll on the first roll of the next frame. If they knock down all ten pins on the first roll of a frame (a "strike"), the frame is over and it gets ten points plus the next two rolls.

A naive implementation of our scoring function could look something like this:


function scoreGame(rolls) {

  let newFrame = true
  let score = 0

  rolls.forEach((roll, i) => {
    // in case of a strike
    if (roll === 10) {
      score += (10 + rolls[i+1] + rolls[i+2])
      newFrame = true // end frame
      return // return from callback, go to next roll
    }

    // first roll of a non-strike frame, move on
    if (newFrame) {
      newFrame = false
      return // return from callback, go to next roll
    }

    // score a full frame
    let frameScore = roll + rolls[i-1]

    // if it's a spare, add next roll
    if (frameScore === 10) {
      frameScore += rolls[i+1]
    }

    score += frameScore
    newFrame = true
  })

  return score
}

If this doesn't return the value you're expecting, where would you start looking for the defect? You'd probably have to log out all of the rolls and compare their values to the running total, trying to infer the problem from there. That gets pretty complex pretty quickly when you consider all of the conditions for scoring individual frames.

Let's look at a solution that will make your debugging a little easier and your code a bit more reliable:


function scoreGame(rolls) {
  const frames = getFramesFromRolls(rolls)

  let score = 0
  frames.forEach((frame, i) => {
    score += scoreFrame(frame, frames[i+1], frames[i+2])
  })
  return score
}

function getFramesFromRolls(rolls) {
  // takes list of rolls [ 1, 3, 6, 3, 10, 5, 3 ]
  // and returns frames [ [1,3], [6,3], [10], [5,3] ]
}

function scoreFrame(frame, nextFrame, nextNextFrame) {
  // takes current and next two frames, and returns score of current frame
  // eg: [5,3] -> 8
  //     [5,5], [3,5] -> 13 (spare: 10 points plus next roll)
  //     [10], [10], [4,2] -> 24 (strike: 10 points plus next two rolls)
}

Now we've broken our complex behavior into two distinct sub-behaviors.

First, we want to turn a list of rolls ([3, 5, 10, 4, 3, 2, 7]) into a list of frames ([3, 5], [10], [4, 3], [2, 7])

Then, we'll score each frame (passing the following two frames in case of strikes or spares), and simply add up all the frame scores.

I've left the implementation as an exercise for the reader. If you do implement these two functions, you'll probably end up with a few more total lines of code than just the monolithic function. But you'll have gained two additional points of verification in your code. This makes debugging and reasoning about the function much easier. You first concentrate on just getting the frame list, and you make sure that function is working correctly. Then you concentrate on the scoring of an individual frame. Then you combine those to functions into a simple function to score the entire game.

At that point, if you're not getting the right return values from scoreGame, you can first check that getFramesFromRolls is giving back the list of frames that you expect, then you can verify that the scoreFrame function is returning the correct score for each individual frame. If one of those is misbehaving, you now have a smaller area to look for the bug. And if both of those appear to be working correctly, then chances are it's the scoreGame function itself, which is only 5 lines long.

Tomorrow we'll start looking at a repeatable system to apply when debugging.


Did you like this?

I send a daily email with tips and ideas like this one. Join the party!

Icon