Null: The Billion Dollar Mistake

Null: The Billion Dollar Mistake

He is not your friend. It does not simplify life or make you more efficient. Just more lazy. It is time to stop using null

TL;DR: The simple mistake that is causing a lot of problems.

Tearing Down Another Inveteracy

You have been heavily using null.

It is comfortable, efficient, and fast, yet you have suffered a bazillion problems related to its use.

What cognitive bias currently prevents you from recognizing the problem and starting to solve it?

What Does Null Stand For?

Null is a flag. It represents different situations depending on the context in which it is used and invoked.

This yields the most serious error in software development: Coupling a hidden decision in the contract between an object and who uses it.

As if this were not enough, it breaks the bijection which is the more important design rule.

Representing multiple elements of the domain with the same entity forces you to have contextual interpretations.

A good software principle challenges you to have high cohesion.

All objects should be as specific as possible and have a single responsibility (The S for Solid).

The least cohesive object of any system is your wildcard: null


Null is mapped to several different concepts in the real-world.

Catastrophic Failures

Null is not polymorphic with any object, so any function that invokes it will break the chain of subsequent calls.

Example 1: Let's model the interaction between people during the COVID-19 pandemic.

<?

final class City {
   public function interactionBetween($somePerson, $anotherPerson) {
       if ($this->meetingProbability() < random()) {
          return null; // no interaction       
       } else {
          return new PersonToPersonInteraction(
             $somePerson, $anotherPerson);
       }
    }
}

final class PersonToPersonInteraction {
   public function propagate($aVirus) {
       if ($this->somePerson->isInfectedWith($aVirus) 
           && $aVirus->infectionProbability() > random()) {
           $this->anotherPerson->getInfectedWith($aVirus);
       }
   }
}

$covid19 = new Virus();
$janeDoe = new Person();
$johnSmith = new Person();
$wuhan = new City();

$interaction = $wuhan->interactionBetween($johnSmith, $janeDoe);
if ($interaction != null) {
    $interaction->propagate($covid19);
}

/* In this example we modeled the interaction 
between an infected person and a healthy one.
Jane is healthy but might be infected 
if Virus R0 applies to her. */        

You can see two null flags and the corresponding if clause.

Null propagation seems to be contained, but looks are deceiving.

A Little Bit of History

The creation of null happened due to a fortuitous event in 1965.

Tony Hoare: The creator of the QuickSort algorithm and also a winner of the Turing Prize (the Nobel Prize in Computing), added it to the Algol language because it seemed practical and easy to do.

Several decades later, he showed his repentance:

This excellent article tells the story in detail:

I call it my billion-dollar mistake...At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. -- Tony Hoare, inventor of ALGOL W.

The full video is also available here.

The Excuse

You use null because it is easy (to write down) and because you believe it improves the efficiency of your software.

By making this mistake, you ignore that the code is read up to 10 times more than it is written.

Reading code with nulls is arduous.

Therefore, you are only postponing the problem later.

Regarding efficiency (which is the most used excuse to generate coupling).

Unless in very specific and critical cases, its performance loss is negligible.

It is just justified in those systems that prioritize efficiency over readability, adaptability, and, maintainability (there's always a trade-off regarding quality attributes).

This cognitive bias persisted over time although according to the current state of the art, modern virtual machines optimize the code for you.

To use evidence instead of gut feelings, you need to start benchmarking instead of erroneously claiming that efficiency is more important than readability.


Fail Fast

Null is (ab)used to mask unexpected situations and spread the error in the code too far away, generating the much-feared ripple effect.

One of the principles of good design is to fail fast.

Example 2: Given a data entry form for a patient, you are requested to fill in the date of birth.

If there is an error in the visual component and the object creation, it could be built with a null date of birth.

When running some nightly batch process that collects all the dates of the patients to calculate an average age, the admitted patient will generate an error.

The stack with useful information for the developer will be very far from where the defect is present. Happy debugging!

Photo by Victoria Heath on Unsplash

What is more, there might be different systems with different programming languages, data transmission through an API, files, etc.

The developer's nightmare is having to debug that defect early in the morning and try to find the problem's root cause.

Incomplete objects

Null is used in many ways, as listed above.

If you allow incomplete models, non-completeness is usually modeled with a null.

This adds complexity by populating the code with controlling ifs.

The presence of nulls generates repeating code and messes up the code with multiple ifs controls.

Fostering incomplete models forces us to make two additional mistakes:

1. Pollute code with setters to complete the essential information needed.

2. Build mutable models violating bijection ignoring real-world entities that do not mutate their essence.

Typed languages that do not handle optionally

Most typed languages prevent errors by ensuring that the object that is sent as a parameter (or returned) can answer a certain protocol.

Unfortunately, some of these languages have taken the step backward of allowing to declare that the object is of a certain type and (optionally) null.

This breaks the chain of invocations forcing put Ifs to control the absence of the object violating the Solid open/closed principle.

What's more, null corrupts type controls. If you use a typed language and trust the compiler defense network.

Null manages to penetrate it like a virus and spread to the other types as pointed out below.

The solution

Do not use it.

The alternatives

As usual to solve all your problems, you should stay loyal to the only axiomatic design rule.

Search the problem domain for solutions to bring them to your model.

Model polymorphic absences

In the above case when objects must declare a type, there are more elegant solutions that avoid ifs to model optionally.

In classification languages, it is enough to use the NullObject design pattern in your concrete class sibling and declare the supertype as a type of the collaborator based on the Liskov substitution principle (L of SOLID).

However, if you decide to implement that solution you will be violating another design principle stating:

You should subclassify for essential reasons and not reuse code or adjust class hierarchies.

The best solution in a classification language is to declare an interface to which both the real class and the null object class must adhere.

In the first example:

<?

Interface SocialInteraction {
    public function propagate($aVirus);
}

final class SocialDistancing implements SocialInteraction {
    public function propagate($aVirus) { 
    // Do nothing !!!!
    }
}

final class PersonToPersonInteraction implements SocialInteraction {
   public function propagate($aVirus) {
       if ($this->somePerson->isInfectedWith($aVirus) 
           && $aVirus->infectionProbability() > random()) {
              $this->anotherPerson->getInfectedWith($aVirus);
       }
   }
}

final class City {

    public function interactionBetween($aPerson, $anotherPerson) {
        return new SocialDistancing(); 
        // The cities are smart enough to implement
        // social distancing to model Person to Person interactions
    }
}

$covid19 = new Virus();
$janeDoe = new Person();
$johnSmith = new Person();
$wuhan = new City();

$interaction = $wuhan->interactionBetween($johnSmith, $janeDoe);
$interaction->propagate($covid19);

/* Jane will not be affected since the interaction
 prevents from propagating the virus */        

No viruses are involved and neither ifs nor nulls!

In this example, null is replaced with a specialization that, unlike it, exists in the problem domain.

The patient's birth date revisited

Let's go back to the patient's form example. You needed to compute the average leaving out not filled forms.

<? 

Interface Visitable {
    public function accept($aVisitor);
}

final class Date implements Visitable {
    public function accept($aVisitor) {
        $aVisitor->visitDate($this);
    }
}

final class DateNotPresent implements Visitable {
    public function accept($aVisitor) {
        $aVisitor->visitDateNotPresent($this);
    }
}

final class AverageCalculator {
    private $count = 0;
    private $ageSum = 0;

    public function visitDate($aDate) {
        $this->count++;
        $this->ageSum += today() - $aDate;
    }

    public function visitDateNotPresent($aDate) {
    }

    public function average() {
        if ($this->count == 0)
            return 0;
        else
            return $this->ageSum / $this->count;
    }
}

function averagePatientsAge($patients) {
    $calculator = new AverageCalculator();
    foreach ($patients as $patient)
        $patient->birthDate()->accept($calculator);
    return $calculator->average();
}        

This example uses Visitor pattern to navigate objects that can behave as null objects.

No nulls

Furthermore, you removed non-essential if using polymorphism and left the solution open to other besides average calculations through the open/closed principle.

You build a less algorithmic but more declarative, maintainable, and extensible solution.

Use languages with explicit modeling of absences

Some languages support optionally the concept of Maybe/Optional which is a particular case of the proposed solution implemented above at the language level.

Conclusions

Using null is a discouraged practice based on deep-rooted practices in the industry.

Despite this, almost all commercial languages allow it and developers use it.

You should, at least, begin to question its use and be more mature and responsible in developing software.


Part of the objective of this series of articles is to generate spaces for debate and discussion on software design.

We look forward to comments and suggestions on this article.

This article is also available in Spanish here, and Chinese here.

要查看或添加评论,请登录

Maximiliano Contieri的更多文章

  • Refactoring 001 - Remove Setters

    Refactoring 001 - Remove Setters

    Setters violate immutability and add accidental coupling TL;DR: Make your attributes private to favor mutability…

  • Code Smell 292 - Missing Return

    Code Smell 292 - Missing Return

    When your code loses its way TL;DR: Missing return statements cause unexpected behavior. Problems ?? Silent failures…

  • Code Smell 291 - Mutable Keys

    Code Smell 291 - Mutable Keys

    Changing Keys, Losing Values TL;DR: When you use mutable objects as keys in hashed collections, changing them breaks…

    2 条评论
  • Refactoring 023 - Replace Inheritance with Delegation

    Refactoring 023 - Replace Inheritance with Delegation

    Transform your rigid inheritance into flexible delegations TL;DR: Replace restrictive inheritance hierarchies with…

  • 16 Simple Tips to Boost Your Productivity x10

    16 Simple Tips to Boost Your Productivity x10

    TL;DR: More productivity tips These easy-to-follow tips will improve your output dramatically. 1.

  • Code Smell 290 - Refused Bequest

    Code Smell 290 - Refused Bequest

    When you ignore your inheritance, you will have trouble with your parents TL;DR: Subclasses should honor ALL their…

  • Code Smell 289 - Shy Visitor

    Code Smell 289 - Shy Visitor

    Don't knock. You are accepted TL;DR: Avoid combining the Visitor pattern with instanceof checks.

    1 条评论
  • Code Smell 288 - Unthrown Exceptions

    Code Smell 288 - Unthrown Exceptions

    TL;DR: Creating a new exception without throwing it leads to silent failures. When You Forget to Throw, Your Code Will…

    1 条评论
  • Code Smell 287 - Unused Local Assignment

    Code Smell 287 - Unused Local Assignment

    Are you using the returned value? TL;DR: Avoid assigning values you never use. Problems Dead code Unclear intent…

  • Refactoring 022 - Extract Common Ancestor

    Refactoring 022 - Extract Common Ancestor

    Make your class hierarchy clear and flexible TL;DR: Extract a common abstract class to mimic real-world structure…

社区洞察

其他会员也浏览了