Table of contents
- Tearing down another inveteracy
- The solution
- The alternatives
He is not our friend. It does not simplify life or make us more efficient. Just more lazy. It is time to stop using null.
Tearing down another inveteracy
All of us have been heavily using null.
It is comfortable, it is efficient, it is fast and yet we have suffered a bazillion of problems related to its use. What is the cognitive bias that currently prevents us, in addition to recognizing the problem, to start solving 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 that was our only design rule.
Representing multiple elements of the domain with the same entity and forcing us to have contextual interpretations.
A good software principle challenges us to have high cohesion. All the objects should be as specific as possible and have a single responsibility (The S for Solid).
The least cohesive object of any system is our wildcard: null
Null is mapped to several different concepts in the real world
Null is not polymorphic with any object so any function that invokes it will break the chain of subsequent calls.
Example 1: Lets us model the interaction between people during the current covid-19 pandemic.
We can see there are 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.
We, as developers, use null because it is easy (to write down) and because we believe it improves the efficiency of our software. By making this mistake we ignore that the code is read up to 10 times more than it is written. Reading code with nulls is more arduous and difficult. Therefore, we 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. And 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 us. To use evidence instead of gut we just need to start benchmarking instead of keep on erroneously claiming that efficiency is more important than readability.
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 we 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!
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 bug early in the morning and try to find the problem's root cause.
Null is used in many ways, as we listed above. If we 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:
- Pollute code with setters to complete the essential information needed.
- 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) is able to 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 we 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.
Do not use it.
As usual to solve all our problems we should stay loyal to the only axiomatic design rule that we have imposed ourselves.
Search the problem domain for solutions to bring them to our 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 our concrete class sibling and declare the supertype as a type of the collaborator based on the Liskov substitution principle (L of SOLID).
However, if we decide to implement that solution we will be violating another design principle stating:
We should subclassify for essential reasons and not to 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:
No viruses are involved and neither ifs nor nulls!
In this example, we replaced null with a specialization that, unlike it, exists in the problem domain.
Patient's birth date revisited
Let's go back to the patient form example. We needed to compute the average leaving out not filled forms.
We used the Visitor pattern to navigate objects than can behave as null objects.
Furthermore, we removed non-essential if using polymorphism and left the solution open to other besides average calculations through the open/closed principle. We 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.
Using null is a discouraged practice based on deep-rooted practices in our industry. Despite this, almost all commercial languages allow it and developers use it. We 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.