Code Readability Part 1

on

Stay tuned, I'll shortly be releasing a transpiler that helps improve code readability in C#

What is readable code?

I've been programming for how many years now? But when it comes to readable code I'm asking myself, how? This has got to be a very bad sign that I'm finding "readable code" so hard to define.

Here's a naive first attempt:

"A programmer that understands the programming language at least as well as yourself can read the code with a fresh pair of eyes (ie: you revisit your own code later after forgetting everything about it) and read the code line by line and every line of code is completely understandable at a quick glance, without feeling cognitively lost about what happened in the code that came before it (ie: if you do forget what happened before, it should be easily remembered by taking another quick glance at the code before it)." (Quite the pipe dream)

You might be thinking, "writing readable code really isn't that hard for me". Please consider rethinking your confidence in the matter, especially since you will inevitably be re-reading it one day.

Do a quick thought experiment, pretend you just finished writing new code that you are about to commit to source control, right now at this moment in time you might be able to easily read through and understand all that your code is doing, after all you just barely wrote it, but does that mean that a year from now you'll be able to read it just as easily? You very well might be writing code that makes sense in your current frame of mind but years from now it could be totally confusing and ambiguous. Stop and think for a moment, pretend you are clueless about all the code you are looking at, what does your code look like now?

Macro vs Micro Level

As you might imagine you could find yourself reading code at a very micro-level, for instance just reading the code found within the scope of a small function. Or you could be reading code at a macro-level, for instance you might by trying to read the code from start to finish of an entire program and you're jumping across classes and methods as you read. To write readable code, techniques that maximize both macro and micro level readability must be considered.

OOP, while certainly pertains to both micro and macro reading levels most current OOP principles being taught appear to encourage maximizing micro-level reading, for instance classes with single responsibilities, functions are recommended to be short with single purpose, etc. The OOP code I've read that is written in such a way does come at a macro-level reading cost. To see how it all connects together is going to require quite a bit of jumping around while trying to read through it all, but spending time trying to understand one particular class might not be so difficult.

Micro-level readable code

The best book on this topic that I've read is "The Art of Readable Code" by Dustin Boswell and Trevor Foucher. The book is filled with lots of code samples and they analyze the different components of what makes a particular code sample easy to read or not. It does not look at code at a macro-level, but that isn't the book's purpose, it even states it does not cover "the overall architecture of your project or your choice of design patterns." It's unfortunate that it doesn't as that is extemely important too, but it does do an excellent job at teaching how to write readable code at a micro-level.

Before addressing macro-level readability tips, I'll cover a few micro-level tips first.

Less Code

Less code is preferred as long as it's readable because it gives you the obvious benefit that less code is less you have to read and less you have to remember in your head.

Rule #1 - When code can be concise and is just as readable as the longer form then use the concise version.

Rule #2 - When code is less readable when concise then use the longer version.

Below I've shown a code snippet from "The Art of Readable Code" book. I chose this snippet since it is not very easy to read quickly. In the book this example is contrasted to a much easier to read version that uses a for loop. As I looked closer at this code I realized the while loop version could also be made to be more readable using the less code principle. Here is the original version:

if (node == NULL) return;

while (node->next != NULL) {
    Print(node->data);
    node = node->next;
}
if (node != NULL) Print(node->data);

This code can be simplified to improve the speed at which it is understood. These extra not null checks aren't necessary (notice how the while loop now checks `node` for NULL instead of `node->next`).

while (node != NULL) {
    Print(node->data);
    node = node->next;
}

Notice that in this case less code is less to read really helps out a lot. Be on the lookout for code improvements done via code elimination, especially if you are taking longer than you should have to when revisiting a particular code segment.

Early Return

Returning early is a technique I've seen commonly used by a lot of coders. For those of you who aren't familiar with it or haven't programmed this way before if might feel a bit counterintuitive and odd since it involves additional return statements, but the obvious benefit that holds greater readability weight is that your brain can move on after the early return conditions and not have to think about them as it moves on to read the code that follows. It even can help code to be less nested since an if-return can be done instead of an if-else, the else statement is unnecessary.

Here is a simple example but even with a simple example you can start to see the readability difference.

void MoveIssueStatusForwardOneState(Issue issue) {
    if (issue.Status != Status.Closed)
    {
        string prevStatusName = issue.StatusName;

        issue.Status++;
        issue.StatusName = this.StatusNames[issue.Status];

        string updateMsg = $"Updated status from {prevStatusName} to {issue.StatusName}";
        this.Log(updateMsg);

        if (issue.Stats == Status.Done)
            this.DoneCount++;
    }
}

With early return:

void MoveIssueStatusForwardOneState(Issue issue) {

    if (issue.Status == Status.Closed)
        return;

    string prevStatusName = issue.StatusName;

    issue.Status++;
    issue.StatusName = this.StatusNames[issue.Status];

    string updateMsg = $"Updated status from {prevStatusName} to {issue.StatusName}";
    this.Log(updateMsg);

    if (issue.Status == Status.Done)
        this.DoneCount++;
}

Notice the code after the early return has 1 less indendation level and already aids in code readability as there isn't a condition to consider before those lines of code. The fact that the status check for if it is closed is already handled by the time you get to the code after it and you don't have to even think about it anymore.

If we were later asked to add in another status that can't be updated, like let's say "Resolved", since that edge-case handling is handled up front it is a lot cleaner to add it in without hurting readability the same way the first version does.

if (issue.Status != Status.Closed && issue.Status != Status.Resolved)
{
    ...
vs:
if (issue.Status == Status.Close || issue.Status == Status.Resolved)
    return;
...
In version 1 above it now has to consider 2 conditions that you have to remember about while reading the code block.

Learn about macro-level code readability

I'm currently investigating this myself and I'll be presenting more information about this in upcoming posts in this series. I found a research article that looks at a code readability of a full program (not a large one though), but it still reveals useful information. I'll be covering this research article in an upcoming post, but if you can't wait and want to read it now search for this paper: "Program Readability: Procedures Versus Comments".

Additionally I've been building a simple transpiler that helps organize code to improve macro-level code readability (currently implemented for C#), so please keep visiting my blog as I'm planning on releasing it soon.

Recap