DEV Community

Cover image for Dealing with Nothing in C# - The Null Object Pattern
Botond Balázs
Botond Balázs

Posted on

Dealing with Nothing in C# - The Null Object Pattern

This is the English version of my original post in Hungarian. Photo by Samuel Zeller on Unsplash.

The null reference is so ubiquitous, such an integral part of our experience as programmers, that it would be hard to imagine a world without it. Yet it is the source of so much frustration and actual financial loss that it is well worth the effort to think about some alternatives.

But what exactly is the problem?

Let's imagine we have a system where customers can place orders. The orders contain line items that have a reference to a product and they store the quantity ordered. They can also calculate the price based on the unit price and the quantity specified.

public class LineItem
{
    public Product Product { get; }
    public decimal Quantity { get; }
    public decimal Price => Product.UnitPrice * Quantity;

    public LineItem(Product product, decimal quantity)
    {
        if (quantity <= 0)
            throw new ArgumentOutOfRangeException(nameof(quantity));
        if (product == null)
            throw new ArgumentNullException(nameof(product));

        Product = product;
        Quantity = quantity;
    }
}
Enter fullscreen mode Exit fullscreen mode

A single discount of various types can be applied to an order so we use the Strategy pattern to decouple orders from the calculation of discounts.

public interface IDiscount
{
    decimal Calculate(decimal orderTotal);
}

public class CouponDiscount : IDiscount
{
    public CouponDiscount(string couponCode, decimal rate) { /* ... */ }

    public decimal Calculate(decimal orderTotal) => orderTotal * rate;
}

public class FrequentBuyerDiscount : IDiscount { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Given the order total, a discount object can calculate the reduced price according to its own set of rules.

The order class provides methods to add line items and a discount, and a property that returns the total after applying the discount.

public class Order
{
    public IEnumerable<LineItem> LineItems => lineItems.AsReadOnly();
    public decimal Total => discount.Calculate(totalBeforeDiscount);

    private decimal totalBeforeDiscount => lineItems.Sum(i => i.Price);
    private IDiscount discount; // initialized to null
    private List<LineItem> lineItems;

    public Order()
    {
        lineItems = new List<LineItem>();
    }

    public void AddLineItem(LineItem lineItem)
    {
        if (lineItem == null) throw new ArgumentNullException(nameof(lineItem));

        lineItems.Add(lineItem);
    }

    public void AddDiscount(IDiscount discount)
    {
        if (discount == null) throw new ArgumentNullException(nameof(discount));

        this.discount = discount;
    }
}
Enter fullscreen mode Exit fullscreen mode

The class above seems completely innocent. What can go wrong? We even added null checks for all the arguments. But it is still very likely that it will throw a NullReferenceException. Can you spot the bug?

var order = new Order();

order.AddLineItem(new LineItem(product1, 2));
order.AddLineItem(new LineItem(product2, 1));

Console.WriteLine($"Total: {order.Total}"); // BOOM!!!
Enter fullscreen mode Exit fullscreen mode

Oh, we forgot to add a discount!

But wait. Creating an order without a discount is a completely valid requirement. Let's fix the code to support that.

public decimal Total =>
    discount == null ? totalBeforeDiscount : discount.Calculate(totalBeforeDiscount);
Enter fullscreen mode Exit fullscreen mode

This code works, but it sure isn't beautiful. Even if we put aesthetic concerns aside, having to check for null every time we access the discount field is a recipe for disaster. Forget it even once, and a NullReferenceException appears. Also, we need to duplicate the default logic, returning the total before discount, every time. That logic isn't well encapsulated.

Fortunately, we can do better. Let's look at how we dealt with the exact same problem when adding the line items. We initialized the lineItems field to a safe default value, the empty list, in the constructor.

public Order()
{
    lineItems = new List<LineItem>();
}
Enter fullscreen mode Exit fullscreen mode

Having done that, there is no need to check whether lineItems is null in the AddLineItem method.

lineItems.Add(lineItem);
Enter fullscreen mode Exit fullscreen mode

Can we imagine such a safe default value for discount too? Of course we can, that would be a NoDiscount class in which the Calculate method simply returns the orderTotal argument without modification.

public class NoDiscount : IDiscount
{
    public decimal Calculate(decimal orderTotal) => orderTotal;
}
Enter fullscreen mode Exit fullscreen mode

Such a default object is called a Null Object. Now we can use that as the initial value for discount in the constructor,

public Order()
{
    lineItems = new List<LineItem>();
    discount = new NoDiscount();
}
Enter fullscreen mode Exit fullscreen mode

and get rid of the problematic null check in the Total property.

public decimal Total => discount.Calculate(totalBeforeDiscount);
Enter fullscreen mode Exit fullscreen mode

As a final piece of advice, it is recommended to exercise restraint when applying the Null Object pattern. It is a very elegant solution when the default behavior is harmless and predictable. But if the behavior of the Null Object is not obvious to the consumer, getting a NullReferenceException is actually more useful than having a badly-designed Null Object behave unexpectedly.

Latest comments (18)

Collapse
 
n_develop profile image
Lars Richter

Nice post. I'm a huge fan of the "Null Object" and try to avoid any null in my code as good as possible. Of course, you have to consider null as possible inputs from external sources (network, database, user input), but inside of my own code, I use "Null Object" most of the time.

Collapse
 
jamesmh profile image
James Hickey

Very well written and straightforward - thanks!

Using the same pattern but in the context of collections, C# has ways to define "empty" collections. For example, a collection of IEnumerable can use the Null Object pattern this way:

IEnumerable<SomeModel> collection = Enumerable.Empty<SomeModel>();

Looking forward to the next article!

Collapse
 
dance2die profile image
Sung M. Kim

Thank you Botond for providing a concrete example to make it easier to digest the Null Object pattern (NOP hereafter since it's too long to type 😉).

I've been using NOP happily and worked great when used in conjunction with Singleton Pattern (😱).

Shameless plug: I've touched on it briefly here.

Collapse
 
danimischiu profile image
Dani Mischiu

Well explained and easy to follow. Thank you!

Collapse
 
cosminpopescu14 profile image
Cosmin Popescu

Well explained. I followed it !

Collapse
 
j2jensen profile image
James Jensen

Very good overview of the null object pattern.
Others already mentioned Maybe/Optional pattern. Here's my take on that pattern: github.com/j2jensen/callmemaybe
I'm definitely looking forward to nullable reference types in C# 8!

Collapse
 
jhbertra profile image
Jamie Bertram • Edited

Interesting pattern - for a more general purpose solution to the null-safety issue when no default can be defined, I'd recommend the Maybe / Option pattern, which is a universally equivalent (but type-safe) alternative to null references

Collapse
 
jvarness profile image
Jake Varness

This is awesome!

Collapse
 
rubberduck profile image
Christopher McClellan

The next version of C# will make huge strides toward making null a nonissue. Nullable reference types.

Collapse
 
slavius profile image
Slavius • Edited

AFAIK there are currently measures to deal with null issues in C#

They are:

Nullable types System.Nullable<T>

int? Id = null;

Default literal

int i = default;

Null coalescing operator

int currentUserId = getUserId() ?? -1;

Null conditional operator

var userObject = null;
try {
   ...
}
catch {
   ...
}
finally {
  userObject?.Dispose();
}
Collapse
 
rubberduck profile image
Christopher McClellan

Nullable value types introduced the concept of null where previously a value couldn’t be null. (It’s essentially an option type.) The rest of these were introduced as syntactic sugar to help us deal with the fact that null exists in the language. In upcoming versions of C# you will be able to opt into nullability. Soon, the default mode of operation in C# will be “things can’t be null”.

Thread Thread
 
slavius profile image
Slavius

The benefit comes when you use Resharper because you can utilize code annotation attributes ([CanBeNull], [NotNull], [ItemCanBeNull], [ItemNotNull])

Thread Thread
 
rubberduck profile image
Christopher McClellan

So that’s exactly what I think you’re missing here. In upcoming versions, you won’t need those annotations. Take some time to look into Nullable reference types.

Thread Thread
 
slavius profile image
Slavius

I think it's far from being ready. Especailly if it introduces breaking changes. Switching midsets is difficult and if you hide a feature behind a compiler switch it's not going to work.

By the way John Skeet found 2 bugs straight away...

Collapse
 
balazsbotond profile image
Botond Balázs

Awesome, I knew they were working on it but I didn't know they already have a working prototype I can download!

Collapse
 
boldwade profile image
Wade Walker

Great read, Thanks!

Collapse
 
makingloops profile image
Joe Petrakovich

You laid out the problem and then the solution very clearly. Thank you! Quite a satisfying read.

Collapse
 
balazsbotond profile image
Botond Balázs

Thanks Joe!