Understanding Cost

As engineers it is vital that we can reason about and have intellectual control over our code. We do this through clean code and focusing on structure, naming, avoiding duplication and so on, to improve readability and keep costs down. The link between readability and cost is clear. Readable software is easier to work with and has fewer bugs than poorly written software. Readability really matters.

Improving readability can mean one of several things: reorganising existing elements (perhaps through renaming), removing redundant elements or introducing new elements such as variables, constants, methods or types to better convey intent. But in adding more code are we not increasing overhead and cost? Surely we want less code not more code.

However, more code does not necessarily mean more cost. An easy-to-understand 50 line class that lays out its intentions clearly will cost far less to maintain than a 20 line mess of a class.

Cost is as much about cognitive load as it is about the number of lines of code. A 20 line mess is neither sufficient nor readable – it is just bad code.

So, sufficiency is less about condensing your code into as few statements as possible and more about being understood as succinctly as possible. There is a difference. In that regard, sufficiency and readability are on the same page, working together towards the same goal.

3 Questions

Before adding any new element to your software you should ask yourself 3 questions:

  • Does the element serve a purpose?
  • Does it add value (e.g. increases readability)?
  • And what is the cost of adding the element?

Of the 3, cost is the hardest to measure and agree upon. One person’s readability could be another person’s needless complexity.

Remove Personal Opinion

Thankfully, industry-wide heuristics help remove most of the subjectivity around this. If a block of code is difficult to understand, then pull it into its own method (or type) and give it a name that adds meaning to the code. This is well-trodden ground – avoid code smells, write for other people not the compiler, and all that.

Heuristics will get us most of the way there, but there will always be room for disagreement - grey areas where two thoughtful engineers’ opinions will land them on different sides of the fence. They understand the available choices, and the benefits and trade-offs of each, but perhaps one felt it was too early to introduce a new element (based on the principle that ‘you ain’t going to need it’) and the other thought the additional element was required to improve their understanding of the code.

These differences of opinion can occur but should be resolvable through regular team-wide code reviews and reaching a pragmatic consensus.

The key thing is to be aware of the choices, and of the benefits and trade-offs of those choices. At least be able to make a balanced decision, not an uninformed one.

Adding Elements to Increase Readability

As an example of the pull between sufficiency and readability consider the Bowling Game challenge - a classic programming kata to score one line of American 10 pin bowling. Think frames, strikes, spares and sore fingers. Here is a 13 line solution that accepts a sequence of pins for a single line. It assumes that the sequence is valid.

    public static int score(int... pins) {
        int score = 0;
        int frameIndex = 0;
        for (int frameCount = 0; frameCount < 10; frameCount++) {
            int frameScore = pins[frameIndex] + pins[frameIndex + 1];
            if (frameScore >= 10) {
                frameScore += pins[frameIndex + 2];
            }
            score += frameScore;
            frameIndex += pins[frameIndex] == 10 ? 1 : 2;
        }
        return score;
    }

The code works, is reasonably sufficient but is it easy to understand? The intent is obscured behind maths and magic numbers.

To improve things, we refactor the body of the loop into several methods, each with a name that describes its intent:

    public static int score(int... pins) {
        int score = 0;
        int frameIndex = 0;
        for (int frameCount = 0; frameCount < 10; frameCount++) {
            score += frameScore(frameIndex, pins);
            frameIndex += frameSize(pins, frameIndex);
        }
        return score;
    }

The score method is now shorter and easier to understand, but the overall size of the class has increased. And that is okay, because revealing intention trumps the number of elements every time.

    public static int score(int... pins) {
        int score = 0;
        int frameIndex = 0;
        for (int frameCount = 0; frameCount < 10; frameCount++) {
            score += frameScore(frameIndex, pins);
            frameIndex += frameSize(frameIndex, pins);
        }
        return score;
    }

    private static int frameScore(int frameIndex, int[] pins) {
        int frameScore = pins[frameIndex] + pins[frameIndex + 1];
        if (frameScore >= 10) {
            frameScore += pins[frameIndex + 2];
        }
        return frameIndex;
    }

    private static int frameSize(int frameIndex, int[] pins) {
        return pins[frameIndex] == 10 ? 1 : 2;
    }

But the question remains, just how far should we take this? Do we need to factor out every decision into its own method or type? Where does adding more so-called intention revealing code stop and adding needless complexity start?

In the case of the Bowling Game, we could introduce code that better explains the key concepts of the game: strikes, spares, open frames:

public class BowlingGame {
    private int[] pins;
    private int frameIndex;

    public int score(int...pins) {
        int score = 0;
        this.pins = pins;
        this.frameIndex = 0;
        for (int frameCount = 1; frameCount <= 10; frameCount++) {
            score += frameScore();
            frameIndex += frameSize();
        }
        return score;
    }

    private int frameScore() {
        if (isStrike()) {
            return tenPlusNextTwoThrows();
        } else if (isSpare()) {
            return tenPlusNextThrow();
        }
        return openFrameScore();
    }

    private int frameSize() {
        return isStrike() ? 1 : 2;
    }

    private int tenPlusNextThrow() {
        return 10 + pins[frameIndex + 2];
    }

    private int openFrameScore() {
        return pins[frameIndex] + pins[frameIndex + 1];
    }

    private boolean isSpare() {
        return openFrameScore() == 10;
    }

    private int tenPlusNextTwoThrows() {
        return 10 + pins[frameIndex + 1] + pins[frameIndex + 2];
    }

    private boolean isStrike() {
        return pins[frameIndex] == 10;
    }
}

The code is more intention revealing, but compromises have been made along the way - we’ve introduced state, sacrificed thread-safety and replaced the static utility score method with an instance method. We don’t necessarily have to make these changes (the actual context may demand that the class must be thread-safe, for example) but this is just to highlight how far we can go in the pursuit of readability.

You need to know when to stop. In the above example, we’ve just increased the line count from 13 to 46. That’s nearly 4 times the original size.

Be careful not to introduce so much ‘intention revealing’ overhead that the code is actually harder to reason about. You have to strike a balance.

This becomes particularly noticeable when you start to introduce new types and push code that once lived together into multiple places. Sometimes this is 100% appropriate but at other times you could be adding accidental complexity and increasing the cognitive load.

Conclusion

Cost and specifically sufficiency and readability can pull in different directions. The trade-off between the two is a deliberate and important choice. Adding code to increase readability is a choice, but so is the decision to remove code for the same effect.

It is entirely possible to have less code, reduce the cognitive load and maintain readability. There is nothing like removing 100 lines of inelegant code with a couple of lines that say the same thing, but in a better way. At the same time, if by adding code you increase readability and reduce cognitive load, go for it.