For something so important, code readability is shockingly ill-defined. We often present it as a grab bag of rules: Use expressive variable names. When a function gets big, split it into smaller functions. Use standard design patterns.
But we’ve all seen code that follows the rules and is still, somehow, a mess.
We might try to solve this problem by piling on more rules: If variable names are getting extremely long, refactor the underlying logic. When one class accumulates tons of helper methods, maybe it should have been two. Don’t use design patterns in contexts where they don’t fit.
The guidelines turn into a maze of judgment calls, requiring a developer who can pick and choose. In other words, a developer who already writes readable code.
So there’s another piece of the puzzle here. To find it, we’ll need to build a broader picture of readability.
What’s readability for, anyway?
In practice, readability usually boils down to “I like reading it.” This isn’t a great heuristic. Besides being subjective, it gets tangled up in our past experiences with reading.
Unreadable code reads like a novel trying to pass itself off as code. Covered in long narrative comments. Files packed with text, to be read in sequential order. Cleverness for the sake of being clever. Lack of word reuse. The code is trying to be readable, but it’s targeting the wrong kind of readers.
There’s a difference between readability and code readability.
Code creates interfaces. But the code itself is also an interface.
Is code readable when it looks pretty? Looking pretty is a nice side effect of readability, but it’s not that useful. Maybe on the margins, it helps with employee retention. But so does a good dental plan. Plus, everyone has a different opinion of what “looks pretty.” Soon enough, this definition of readability spirals into a vortex of tabs, spaces, braces, camel casing, and the other formatting holy wars. No one will faint upon seeing misaligned arguments, despite the attention they attract during code review.
Is code readable when it generates fewer bugs? Having “fewer bugs” is good, but what’s the mechanism here? The warm fuzzy feelings someone gets when they see readable code? Reading can’t summon bugs, no matter how powerfully the reader is frowning at the code.
Is code readable when it’s easier to edit? The ease of editing sounds like the best reason. Requirements change, features get added, bugs appear, and eventually someone will need to edit our code. In order to edit it without causing issues, they need to understand what they’re editing and how their edits will change the behavior.
What makes code easier to edit?
At this point, we might feel a compulsion to rattle off rules again. “Code is easier to edit when the variable names are expressive.” Nice try, but all we’ve done is rename “readability” to “ease of editing.” We’re looking for new insights here, not the same rule-by-rule memorization in a fake mustache and wig.
Let’s start by setting aside the fact that we’re talking about code. Programming has been around for a couple decades, a dot on the timeline of human history. If we restrict ourselves to that dot, we’re drawing our ideas from a shallow well.
Instead, let’s look at readability through the lens of interface design. Our lives are filled with interfaces, digital and otherwise. A toy has features that make it roll or squeak. A door has an interface that lets it open, close, and lock. A book arranges data in pages, allowing for faster random access than a scroll. Formal design training tells us even more about these interfaces; ask your design team for more information. Failing that, we’ve all used good interfaces, even if we don’t always know what makes them good.
Code creates interfaces. But the code itself, together with its IDE, is also an interface. This user interface is aimed at a very small population of users: our teammates. For the rest of this post, we’ll refer to them as “users,” to stay in the headspace of UI design.
With that in mind, consider some sample user flows:
- The user wants to add a new feature. To do this, they must find the right spot and add the feature, without also adding bugs.
- The user wants to fix a bug. They’ll need to find the source of the bug and edit the code so it stops happening, without introducing different bugs.
- The user wants to verify an edge case acts a certain way. They’ll want to find the right code, then trace through the logic to simulate what would happen.
And so on. Most flows follow a similar pattern. We’ll be looking at concrete examples for ease of understanding, but remember, we always want to keep the general principles in mind, rather than falling back to a list of rules.