Unhealthy Code: Null Checks Everywhere!

James Hickey - Sep 5 '19 - - Dev Community

This is an excerpt from my book Refactoring TypeScript: Keeping Your Code Healthy.

Refactoring TypeScript book


Identifying The Problem

Billion Dollar Mistake

Did you know that the inventor of the concept of "null" has called this his "Billion Dollar Mistake!"

As simple as it seems, once you get into larger projects and codebases you'll inevitably find some code that goes "off the deep end" in its use of nulls.

Sometimes, we desire to make a property of an object optional:



class Product{
  public id: number;
  public title: string;
  public description: string;
}


Enter fullscreen mode Exit fullscreen mode

In TypeScript, a string property can be assigned the value null.

But... so can a number property!



const chocolate: Product = new Product();
chocolate.id = null;
chocolate.description = null;


Enter fullscreen mode Exit fullscreen mode

Hmmm....

Another Example

That doesn't look so bad at first glance.

But, it can lead to the possibility of doing something like this:



const chocolate: Product = new Product(null, null, null);


Enter fullscreen mode Exit fullscreen mode

What's wrong with that? Well, it allows your code (in this case, the Product class) to get into an inconsistent state.

Does it ever make sense to have a Product in your system that has no id? Probably not.

Ideally, as soon as you create your Product it should have an id.

So... what happens in other places that have to deal with logic around dealing with Products?

Here's the sad truth:



let title: string;

if(product != null) {
    if(product.id != null) {
        if(product.title != null) {
            title = product.title;
        } else {
            title = "N/A";
        }
    } else {
        title = "N/A"
    }
} else {
    title = "N/A"
}


Enter fullscreen mode Exit fullscreen mode

Is that even real code someone would write?

Yes.

Let's look at why this code is unhealthy and considered a "code smell" before we look at some techniques to fix it.

Is It That Bad?

This code is hard to read and understand. Therefore, it's very prone to bugs when changed.

I think we can agree that having code like this scattered in your app is not ideal. Especially when this kind of code is inside the important and critical parts of your application!


A Side-Note About Non-Nullable Types In TypeScript

As a relevant side note, someone might raise the fact that TypeScript supports non-nullable types.

This allows you to add a special flag to your compilation options and will prevent, by default, any variables to allow null as a value.

A few points about this argument:

  • Most of us are dealing with existing codebases that would take tons of work and time to fix these compilation errors.

  • Without testing the code well, and carefully avoiding assumptions, we could still potentially cause run-time errors by these changes.

  • This article (taken from my book) teaches you about solutions that can be applied to other languages - which may not have this option available.


Either way, it's always safer to apply smaller more targeted improvements to our code. Again, this allows us to make sure the system still behaves the same and avoids introducing a large amount of risk when making these improvements.

One Solution: Null Object Pattern

Empty Collections

Imagine you work for a company that writes software for dealing with legal cases.

As you are working on a feature, you discover some code:



const legalCases: LegalCase[] = await fetchCasesFromAPI();
for (const legalCase of legalCases) {
    if(legalCase.documents != null) {
        uploadDocuments(legalCase.documents);
    }
}


Enter fullscreen mode Exit fullscreen mode

Remember that we should be wary of null checks? What if some other part of the code forgot to check for a null array?

The Null Object Pattern can help: you create an object that represents an "empty" or null object.

Fixing It Up

Let's look at the fetchCasesFromAPI() method. We'll apply a version of this pattern that's a very common practice in JavaScript and TypeScript when dealing with arrays:



const fetchCasesFromAPI = async function() {
    const legalCases: LegalCase[] = await $http.get('legal-cases/');

    for (const legalCase of legalCases) {
        // Null Object Pattern
        legalCase.documents = legalCase.documents || [];
    }
    return legalCases;
}


Enter fullscreen mode Exit fullscreen mode

Instead of leaving empty arrays/collections as null, we are assigning it an actual empty array.

Now, no one else will need to make a null check!

But... what about the entire legal case collection itself? What if the API returns null?



const fetchCasesFromAPI = async function() {
    const legalCasesFromAPI: LegalCase[] = await $http.get('legal-cases/');
    // Null Object Pattern
    const legalCases = legalCasesFromAPI || [];

    for (const case of legalCases) {
        // Null Object Pattern
        case.documents = case.documents || [];
    }
    return legalCases;
}


Enter fullscreen mode Exit fullscreen mode

Cool!

Now we've made sure that everyone who uses this method does not need to be worried about checking for nulls.

Take 2

Other languages like C#, Java, etc. won't allow you to assign a mere empty array to a collection due to rules around strong typing (i.e. []).

In those cases, you can use something like this version of the Null Object Pattern:



class EmptyArray<T> {
    static create<T>() {
        return new Array<T>()
    }
}

// Use it like this:
const myEmptyArray: string[] = EmptyArray.create<string>();


Enter fullscreen mode Exit fullscreen mode

What About Objects?

Imagine that you are working on a video game. In it, some levels might have a boss.

When checking if the current level has a boss, you might see something like this:



if(currentLevel.boss != null) {
    currentLevel.boss.fight(player);
}


Enter fullscreen mode Exit fullscreen mode

We might find other places that do this null check:



if(currentLevel.boss != null) {
    currentLevel.completed = currentLevel.boss.isDead();
}


Enter fullscreen mode Exit fullscreen mode

If we introduce a null object, then we can remove all these null checks.

First, we need an interface to represent our Boss:



interface IBoss {
    fight(player: Player);
    isDead();
}


Enter fullscreen mode Exit fullscreen mode

Then, we can create our concrete boss class:



class Boss implements IBoss {
    fight(player: Player) {
        // Do some logic and return a bool.
    }

    isDead() {
        // Return whether boss is dead depending on how the fight went.
    }
}


Enter fullscreen mode Exit fullscreen mode

Next, we'll create an implementation of the IBoss interface that represents a "null" Boss:



class NullBoss implements IBoss {
    fight(player: Player) {
        // Player always wins.
    }
    isDead() {
        return true;
    }
}


Enter fullscreen mode Exit fullscreen mode

The NullBoss will automatically allow the player to "win", and we can remove all our null checks!

In the following code example, if the boss is an instance of NullBoss or Boss there are no extra checks to be made.



currentLevel.boss.fight(player);
currentLevel.completed = currentLevel.boss.isDead();


Enter fullscreen mode Exit fullscreen mode

Note: This section in the book contains more techniques to attack this code smell!

How To Keep Your Code Healthy

This post was an excerpt from Refactoring TypeScript which is designed as an approachable and practical tool to help developers get better at building quality software.

Refactoring TypeScript book

Keep In Touch

Don't forget to connect with me on:

Navigating Your Software Development Career Newsletter

An e-mail newsletter that will help you level-up in your career as a software developer! Ever wonder:

✔ What are the general stages of a software developer?
✔ How do I know which stage I'm at? How do I get to the next stage?
✔ What is a tech leader and how do I become one?
✔ Is there someone willing to walk with me and answer my questions?

Sound interesting? Join the community!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .