Handling Null at Compile Time
The Option Pattern
Ideally, handling nulls should be done with the compiler. A lot of times, problems are pushed to runtime. By creating a type that represents a nullable object, you can push nulls to the closest thing to a "compile time" check. This pattern is referred to as the Option Pattern. In Java, this is implemented with the object Optional. Here is an example of it:
// Java code
final Optional<Contact> contact = contactDao.find(id);
if (contact.isPresent()) {
sendMessage(contact.get());
} else {
showContactNotFoundMessage();
}
isPresent() becomes your null check. get() is how you actually get the value to use: in this case, a Contact. In Java, the Optional class's Javadoc states that it was intended for method returns only. In other languages, the Option Pattern is a full replacement for null.
Software is expected to be fully expressed with very few types, and there isn't a type for null. Software developed with few types increases bugs, development time, and the amount of unit tests that need to be written. Use the Option Pattern for a value that is nullable. In the case of parameters in methods or constructors, try using method overloading or default values if your language supports it. If you ultimately need a null value, don't use null; use an optional. This would apply for fields as well. Don't use null!
Standardize your code base with the following:
// Java code
public User getUser();
This method signature states this will never be null.
// Java code
public Optional<User> getUser();
This method signature states user will be optional and needs to be handled if there isn't one.
With this standardization, you won't have a question of if something is null anymore.
Throwing Exceptions Instead of Returning Null
When you are calling a method that guarantees an object and that object can't be provided, throw an exception. This should be viewed as a bug in the application and not something that simply needs to be null checked. For example:
// Java code
try {
final var user = userDao.findSuperUser();
// ...
} catch (final MissingSuperUserException e) {
// handle exception
}
This is something that is expected, but not there. An exception is perfect for this use case.
Avoiding Nulls With Collections
When working with collections, it never makes sense to use null. You either have a collection with items, or you have an empty collection. If you are in a situation where null feels like what you need to use, replace it with an empty collection. This same thing would apply to strings. Use an empty string instead of null.
Use the Null Object Pattern
The Null Object Pattern is another way to deal with null. This pattern via polymorphism defines a "null" object, meaning an object that doesn't do anything but stand in for null. This allows the code to execute without any null checks. For example:
// Java code
if (cat == null) {
throw new RuntimeException("Cat object cannot be null.");
} else {
cat.meow();
}
With the Null Object Pattern, a type is defined to represent null. For example:
// Java code
public interface Cat {
void meow();
}
public final class Tiger implements Cat {
@Override
public void meow() {
System.out.println("ROAR!");
}
}
public final class NullCat implements Cat {
@Override
public void meow() {
}
}
Now in the first example, you can remove the null check completely. You will either have an instance of Cat or a NullCat.
Legacy Code and Nulls
When working with legacy code that uses nulls, you'll want to look for a newer replacement that doesn't support these bad practices. If you can't avoid these bad practices in the code, wrap the code so only in your wrapper class(es) will you have to deal with it.
If you are working in legacy code, you may not be able to implement these standards. One thing that you can do to ease the pain of working with nulls is to add the suffix "OrNull" to your methods and variable names. For example:
// Java code
final var contactOrNull = contactDao.findOrNull(name);
It isn't the ideal approach, but it is better than not having anything there to tell the developer.
Conclusion
Even though these aren't "true" compile time checks, it is about the closest you can get, at least in a language like Java. Implementing these across your development team will increase productivity and reduce bugs.