Thoughts on how to write cleaner code

August 05, 2022
post content appropriate preview

Introduction

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”. As Martin Fowler (not so) delicately points out: coding is a communication tool between humans rather than computers. Code should be simple, readable, and easy to maintain.

How much do you value your time? Let me ask you this personal question, assuming you do not have a dishwasher, would you leave your dirty dishes overnight in the sink? Or would you wash them shortly after you are done eating? Would you at least soak them? What I mean is, if you respect your future self well enough, you would save up time by eliminating any hard-to-scrub leftovers from your dishes. In a similar mindset, code shouldn't be ridden with unnecessary complexity, left to dry out, making it difficult to detect and "scrub out" pesky errors. As a result, the codebase becomes hard to read and understand, and much harder to extend or maintain. This is often referred to as "technical debt".

Of course, technical debt is not only about messy code, it's also about service-level architecture decisions, data structures and more. Therefore, we will not analyse this any further on this article. Instead, we will look into a few design patterns and practices which could help us improve our code. To do this, I will use JavaScript with specific examples.

Before you read further, be sure to check out my video presentation on YouTube as well:

Naming

Easy to do, hard to master: a key aspect in reading and understanding code is the appropriate naming of variables, functions, methods and classes. Generally speaking, you want to encapsulate as much meaning in the name, without being too verbose or without using too many acronyms or sounding too generic.

Think of names as something composite, where you can attach meaning in the beginning (prefix) or at the end (suffix) of the core word. To better illustrate this concept, let's imagine a function that fetches a list of items from an endpoint:

// generic verb
const get = () => {
    ...
}

// descriptive name
const getItems = () => {
    ...
}

// further describes a property of the items
const getAvailableItems = () => {
    ...
}

As you can see, a concise function name can communicate parts of the logic encapsulated within itself in addition to what is being returned. This sets the expectations straight and eliminates the need to overload your code with comments. On that topic, even though comments are generally useful because they should describe the purpose of the code, they shouldn't be a second source of truth beyond the code itself. However that's something we may look into on a different article in the future.

Moreover, looking into natural conversations and how we tend communicate with one another, you wouldn't normally use double negation, unless your intent is to be sarcastic or to confuse. For example, if someone asked you "Are you available for a chat later this evening?", you wouldn't normally reply "I am not unavailable" to indicate that you do in fact have time for a chat. Therefore, applying this logic for naming our variables, it's much preferable to write isValid rather than notValid or - even worse - notInvalid for a boolean value.

Of course, the ideas we discussed here should conform and adapt to the corresponding programming language you are coding in, as each of them comes with its own set of rules and conventions. Remember to be consistent!

Guard Clause

I've always liked the name of this pattern. It essentially constitutes an elegant gatekeeper to the flow of your logic. The anti-pattern to this is nested "if ... else" logic. To illustrate both, let's imagine we have a function for validating a form with two inputs: an e-mail address and a message field.

// nested logic
const isFormValid = data => {
    if (isEmailValid(data.email) {
        if (data.hasOwnProperty('message') && data.message.length > 0) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

// guard clause pattern
const isFormValid = data => {
    if (!isEmailValid(data.email)) return false;
    if (!data.hasOwnProperty('message') || data.message.length == 0) return false;
    return true;
}

As you can see, both fields are required, and the e-mail address needs to be validated separately in another function to deem the form as "valid". Now, imagine we wanted to add another required field. By doing so, with the guard clause pattern, we would simply add another line checking for the negation of our condition. On the other hand, following the initial nested logic pattern, we would have to add more lines of code and stretch the width of our document (i.e. number of columns) unnecessarily. As a side-note, we are borrowing one of the naming conventions we talked about earlier in this post.

Furthermore, in some cases, you may hear people refer to the guard clause pattern as "early exit". This is because it has a strong link to the scope of the method: it does not let the flow continue as the context won’t meet the required criteria.By implementing the guard clause pattern where appropriate, you are making your codebase much more readable. As a result, "maintenance costs" and extensibility of the code becomes less of an issue.

Collection (Pipelining)

Do you feel the breeze of functional programming by now? Following our guard clause pattern, I'd like to talk a bit about pipelining, which aims in flattening our loops and sweeping bits of complexity under the rug of "intention".

I am sure most of us work with collections (i.e. processing a list of objects) and, in most cases, we instinctively use a loop. However, as Martin Fowler (https://martinfowler.com/articles/refactoring-pipelines.html) likes to call it, a "collection pipeline" is a valid alternative. What do you think of the code below?

// nested loop
const availableItems = [];
for (const item of items) {
    if (item.count > 0) {
        availableItems.push(item.name);
    }
}

// collection pipeline
const availableItems = items
    .filter(item => item.count > 0)
    .map(item => item.name);

Based on the code above, we are essentially selecting the names of the items in stock (count > 0). What do you think about the pipeline pattern? Far more readable, I would say.

At this point, I feel it would be productive to discuss the "dark side" of this pattern as well. For less experienced developers, this could trick them into underestimating the time complexity of the code. In most cases, pipelines disguise as O(1), while, in reality, the are O(n). Furthermore, an excessive chaining of method calls can quickly lead to a train wreck anti-pattern (http://wiki.c2.com/?TrainWreck). Therefore, use this pattern with caution.

Null Object Pattern

How often do you write code with conditional statements that check for null? Let's look into a simple example:

const itemDescription = item => {
    if (item != null) {
        return `color: ${item.color}, weight: ${item.weight} g`;
    } else {
        return `no description available`;
    }
}

In the code above, we first check whether item is null or undefined, before accessing any of its properties, thus avoiding the error "Uncaught TypeError: Cannot read properties of null ...". Alternatively, we could use a try-catch in place of the "if ... else" logic. However, we can improve the readability of the code by defining a "default" item with same shape as item:

const nullItem = { color: 'unknown', weight: 0 };

const itemDescription = (item = nullItem) {
    return `color: ${item.color}, weight: ${item.weight} g`;
}

As said, the shape of the object is the same, whether the item exists or not, therefore calling item.color or item.weight is safe.

Closing Thoughts

It's important to remember that clean or readable code is not a binary state. This is especially true in the context of time, as newer patterns are discovered and discussed.

Imprint
Copyright © 2023 Threefields Media