Purpose
The Null Object design pattern allows you to eliminate the need for null checks, thereby reducing cyclomatic complexity and enhancing code safety.
The Problem
Consider the following code:
public class ItemView
{
public ItemView(Item item)
{
if (item == null) throw new ArgumentNullException();
// Perform some operations
}
}
It’s common to see null checks like this when working with nullable arguments. However, these checks come at a cost: they increase the cyclomatic complexity of your code.
What is Cyclomatic Complexity?
Cyclomatic complexity is a metric that measures the number of logical branches in your code. In the example above, the code has a cyclomatic complexity of 2 due to the null check. Now, imagine if the code included multiple null checks—complexity would increase significantly even before any substantial work is done.
Additionally, if the program calls ItemView.Render()
without initializing ItemView
, a null reference error could be easily thrown, leading to runtime exceptions.
Visual Representation
Here’s a class diagram that illustrates this scenario:
Now, let’s introduce the concept of a NullItem
class, which can be used in place of a null Item. By implementing both Item
and NullItem
as types of IItem
(an interface), we can avoid null checks altogether.
Implementing the Null Object Pattern
It’s a common convention to prefix null object classes with “Null”. In this case, NullItem would provide a default implementation, ensuring that ItemView never deals with a null value.
Here’s how the code would look using the Null Object pattern:
public class ItemView
{
private readonly IItem _item;
public ItemView(IItem item)
{
_item = item;
}
}
With this approach, the cyclomatic complexity drops to 1, resulting in cleaner and more maintainable code.
Summary
- Avoid Null Checks: The Null Object pattern helps you eliminate the need for null checks.
- Safe Code: The returned object is never null, reducing the risk of null reference exceptions.
- Utilize Interfaces or Inheritance: Implement the pattern using interfaces or inheritance as appropriate for your use case.
- Cleaner Code: The overall code becomes cleaner and more maintainable.