Null Object
08 Jul 2021 - Alfie J.
Alternative Names
- Default Object
Trivia
The null reference was created by Sir Tony Hoare in 1965
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). 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.
Intent
The intent of the null reference pattern is to return a default (but initialised) object for a given class.
It helps to avoid writing common code that looks something like this:
public class Controller
{
private readonly User _user;
public Controller(User user)
{
if (user == null)
throw new ArgumentNullException();
_user = user;
// do something with user
}
}
A null-check is practically a requirement nowadays when accepting nullable arguments into constructors or methods.
Each null check increases the cyclomatic complexity of a method, which is a measure of the number of logical branches in your code. Here, we already have a cyclomatic complexity score of 2, before any actual logic has been written.
Implementation
The null object pattern can be implemented using inheritance, whereby a null version of the object inherits from the object itself:
public class User
{
public virtual string FirstName { get; private set; }
public virtual string Surname { get; private set; }
}
public class NullUser : User
{
public override string FirstName => "FirstName";
public override string Surname => "Surname";
}
or by sharing a common interface with the object:
public class User : IUser
{
public string FirstName { get; }
public string Surname { get; }
}
public class NullUser : IUser
{
public string FirstName => "Alfie";
public string Surname => "Palmer";
}
Usage/Example
Before Null Object
Let's contrive an example whereby we have a Controller object which dumps information about the current user logged into a system.
It's interaction could be modelled as:
private static void Main()
{
var userService = new UserService();
var user = userService.GetCurrentUser();
var controller = new Controller(user);
controller.Dump();
}
The UserService is trivial, it gets the currently logged in user via an internal repository, performs a null check and returns the user if valid.
public class UserService
{
private readonly UserRepository _userRepository = new();
public IUser GetCurrentUser()
{
const int id = 1;
var user = _userRepository.GetUser(id);
if (user == null)
throw new NullReferenceException();
return user;
}
}
The UserRepository is simply mocked and implemented as:
internal class UserRepository
{
private readonly IList<User> _users = new List<User>();
internal UserRepository()
{
_users.Add(new User(1, "Alfie", "Palmer"));
_users.Add(new User(2, "John", "Deere"));
_users.Add(new User(3, "Frank", "Parsons"));
}
internal IUser GetUser(int id)
{
return _users.Any(u => u.Id == id)
? _users.FirstOrDefault(u => u.Id == id)
: null;
}
}
The Controller takes in one of these users as a parameter and sets a field if the user is valid, performing a further three null reference checks:
public class Controller
{
private readonly IUser _user;
public Controller(IUser user)
{
if (user == null)
throw new ArgumentNullException();
if (user.FirstName == null)
throw new ArgumentNullException();
if (user.Surname == null)
throw new ArgumentNullException();
_user = user;
}
public void Dump()
{
Console.WriteLine($"User: {_user.FirstName} {_user.Surname} ({_user.Id})");
}
}
If we build and run the code, we see output:
User: Alfie Palmer (1)
This is fine for any valid id that's passed into GetCurrentUser() on the UserService Imagine our system supports anonymous users - these could be represented by having an internal id of 0. If we change the id to 0 and re-run, we run into a null-reference exception right away and the program crashes.
Adding Null Object
This is where we introduce the Null Object pattern. The code would be a lot less cyclomatically complex and easier to read if we didn't have to check lots of objects (and their fields) for being null. It could also be beneficial to prevent the application (or service) from crashing if the object is null (instead we supply it with a default implementation which is can work with).
We start by creating a class which implements the same IUser interface that User implements:
public class NullUser : IUser
{
public int Id => 0;
public string FirstName => "Anonymous";
public string Surname => "User";
}
We can use this NullUser object in place of returning null in our UserService:
public class UserService
{
private readonly UserRepository _userRepository = new();
public IUser GetCurrentUser()
{
const int id = 0;
var user = _userRepository.GetUser(id);
return user ?? new NullUser();
}
}
We can also go ahead and remove all of the null-checks from the Controller:
public class Controller
{
private readonly IUser _user;
public Controller(IUser user)
{
_user = user;
}
public void Dump()
{
Console.WriteLine($"User: {_user.FirstName} {_user.Surname} ({_user.Id})");
}
}
If we re-run the code example with an id of 0, instead of it throwing a null-reference exception we see the following output:
User: Anonymous User (0)
Benefits
- Reduces the amount of null-checks which need to be performed
- Improves the readability of code
- Reduces the cyclomatic complexity of code
Drawbacks
- Sometimes there is a use case for the object in question to be null
Notes
- Standard convention is to prefix a type with "Null" i.e. NullUser
- Using an interface over inheritance to implement makes the code-depth more shallow and less complex to read
Resources
The material is .7z, which is a 7zip compressed archive which can be unpacked with the 7zip utility.