How to improve code readability

I like to keep my code as clean and legible as possible and there are a few things that have helped me achieve that.

Most, if not all, of them can seem obvious; yet, it is not uncommon to find code that could benefit from a little makeover.

Disclaimer [1]: The following suggestions are not compulsory, but neither is bathing. Think about it.

KYSS: Keep Your Statements Simple

The first step is to follow the KISS principle.

As Albert Einstein said:

Everything should be made as simple as possible, but no simpler.

When writing something, how we write it will have a direct impact on how complex is it perceived. Take the time to ask yourself if your code sparks joy.

I have seen code lines that are not only kilometric[2] but can also span several lines. This also includes nested code. They are all tedious to read and understand.

One typical culprit for this is over-complicated logic that outright begs for a refactor.

It is important to understand that simple does not imply shorter. Simple means a balance between expressiveness and legibility.

When in doubt, follow the Rule of 30 and the Rule of Three.

Local variables are your friends

Raw values, as part of bigger expressions or hardcoded as parameters, should trigger your spidey sense.

Yes, it is always possible to add a comment and explain what they represent. However, code should document itself as best as possible. Why not get rid of those superflous comments and bake them directly into the code by using local variables?

float CalculateCircleArea(float radius)
{
-     // Area = Pi * R^2
-    return 3.14f * radius * radius;
+    const float pi = 3.14f;
+    return pi * radius * radius;
}

Plus, by using local variables, it will be easier to debug the code when using breakpoints.

Best case scenario, a smart compiler will detect that the variable behaves as a constant and will inline it wherever it gets called.

This doesn't mean we should ditch comments, they are still a critical part of the code. Just keep two things in mind:

  • Comments are more easily prone to be outdated than code.
  • The best comments describe the why rather than the what.

Const early, const often

One habit I got into a few years ago was to use const for as many things as possible.

Nowadays, compilers are smart enough to detect which variables can be treated as if they were constant and apply the necessary optimisations. Still, if we can explicitly hint it to the compiler then it's better to do so.

What's great is that we can use this in conjunction with the previous tips, as part of our efforts to declutter the code. Harcoded values are immutable by nature.

For most languages, the developer must explicitely mark variables and functions as const. Other languages, such as Rust, rely on immutability by default.

Make it domain-oriented

The names we give to variables, functions, classes and others, are for the sake of the developer. Once a program is compiled, names stop being important and are replaced with generic placeholders.

By using language specific to the domain (context) of our software, we make it more obvious what we're trying to do. For example, if we had a class containing an array of users, it's better to name a function AddUser than AddToArray. Same thing with everything else that can be named. Even if a variable is supposed to be local and have a lifespan of one line of code, giving it a proper name will make it better.

This can also be done with types. The primitives or basic types of most programming languages are generic enough to represent multiple concepts, yet they can be ambiguous for the very same reason.

Integers and their close relatives are widely used to represent data; for instance, they can be used to represent the state of a process or entity. Saying that a door is in state 0 is barely meaningful, though.

Fortunately, most programming languages have a way to represent integral data in a human-friendly way: enums. And even better, they can also replace booleans in some scenarios[3].

bool b = true;
...
if (b) // Is the door open?
    ...
enum DoorState
{
    Closed,
    Open
}
..
DoorState doorState = DoorState::Closed;
...
if (doorState == DoorState::Closed)
    ...

Granted, it's more verbose[4]. But now we can now make functions that require parameters that will unambiguously represent the state of a door.

Format it

This one is tricky. It borders the line of personal preference and can also be in conflict with code-formatting rules that might already be in place.

The layout of our code can have a great impact on how we understand it. Even if we strip the code away from all meaning and replace it with random characters, the shape of the code can still tell a lot about it.

Here's an example of how I like to format statements that have a lot of operations.

Character* character = ...;
const bool seesCharacters = character && !character->IsInvisible() && IsInFront(character) && IsAlive() && HasLineOfSightTo(character);
Character* character = ...;
const bool seesCharacter = character
                        && !character->IsInvisible()
                        && IsAlive()
                        && IsInFront(character)
                        && HasLineOfSightTo(character);

I also tend to put comments above the statements and not next to it.

So this:

target = Math::Dot(playerFwd, playerToEnemy) > 0.0f ? enemy : nullptr; // Target the enemy is it's in front of the player. 

Becomes this:

// Target the enemy is it's in front of the player. 
target = Math::Dot(playerFwd, playerToEnemy) > 0.0f ? enemy : nullptr;

This whole process can be tedious, which is probably why most people don't do it. However, nowadays, there's plenty of solutions that will automate the formatting of the code. For instance, ClangFormat.

Their main benefit is format standardisation. Formatting is usually a matter of preference, so it's extremely common for different parts of a codebase to look and feel different, depending on who developed or maintains them.

Code formatters guarantee a level of consistency and free the developers from having to remember all of the rules. Their main limitation is that they can only address part of the syntax of the code, not the semantics; in other words, they cannot untangle complex code.


Putting these principles into practice

With the previous things in mind, let's try to improve the following code:

Square s = Square({Vector2D(0.0f, 0.0f), Vector2D(0.0f, 1.0f), Vector2D(1.0f, 1.0f), Vector2D(1.0f, 0.0f)}, Vector3D(255, 0, 0), true);
Circle c = Circle(Vector2D(0.0f, 0.0f), 1.0f, Vector3D(255, 0, 0), false);

It is possible to deduce what most of the parameters in the constructors mean. For the Square type, there are likely: the corners, maybe a color and something that is true (but we can't have an educated guess about what it represents). Similar case for Circle.

Now let's take a look at the improved version.

enum class ShapeFilling
{
    Hollow,
    Full
};

struct Color
{
    unsigned char redChannel;
    unsigned char greenChannel;
    unsigned char blueChannel;
};

const Color colorRed = { 255, 0, 0 };

const List<Vector2D> corners = { Vector2D(0.0f, 0.0f), Vector2D(0.0f, 1.0f),
                                 Vector2D(1.0f, 0.0f), Vector2D(1.0f, 1.0f) };
Square square = Square(corners, colorRed, ShapeFilling::Hollow);
                  
const Vector2D center = Vector2D(0.0f, 0.0f);
const float radius = 1.0f;
Circle circle = Circle(center, radius, colorRed, ShapeFilling::Full);

Despite taking up a few more lines, this solution looks better and is easier to maintain. Furthermore, some of the extra code can be easily reused.


You will find that some of these principles are part of the Zen of Python [5], so be sure to give it a look afterwards.

Programming is communication to both the machine and the developers, so it's seldom a lonely act. And even when it is, we should treat our future selves as a different person that will not necessarily understand the code that is so obvious to us now.

How do you improve the legibility of your code?


  1. Unashamedly taken from Super Meat Boy. ↩︎

  2. The famous column 80 rule. ↩︎

  3. After all, booleans can be interpreted with numbers (0 is False, everything else True). Enums facilitates the representation of uncertainty; we can model the mythical tribool: enum Tribool { False, True, Unknown }. ↩︎

  4. In C++, it is highly recommended to use enum class instead of the C enums. While it's true that having to preprend the values with the name of the enum plus a :: is a mouthful, they prevent the values from being leaked into the rest of the code, possible causing naming conflicts. A positive side-effect is that now multiple enums can now have values named the same, so no need to add weird prefixes to differentiate them. ↩︎

  5. In fact, readability and simplicity are part of the foundations of Python, one of the easiest to use and learn languages today. ↩︎