DEV Community

loading...
Cover image for Kentico Xperience Design Patterns: Modeling Missing Data with The Null Object Pattern

Kentico Xperience Design Patterns: Modeling Missing Data with The Null Object Pattern

Sean G. Wright
Chief Solutions Architect @WiredViews, founding partner @craftbrewingbiz. @KenticoXP MVP. love to learn / teach web dev & software engineering, collecting vinyl records, mowing my lawn, craft 🍺
・Updated on ・10 min read

In a previous post we looked at the importance πŸ€“ of modeling content in Kentico Xperience and the impacts of that content modeling on our code.

We asked the question "How well does our code match the content model? Especially, when working with optional or missing data."

We saw that nullable reference types can help express that data is missing, but they aren't the only tool πŸ”¨ at our disposal for handling or modeling missing data in our Kentico Xperience applications.

In this post we're going to look at one of those alternative tools - the Null Object Pattern.

πŸ“š What Will We Learn?

  • What are the problems with modeling our code using only nullable reference types
  • Implementing the Null Object Pattern with our Call To Action ImageViewModel
  • Where do we fall short with the Null Object Pattern?

πŸƒ A Refresher - Our Call To Action

Let's quickly look back on our example Call To Action Page Type which represents our content model in code:

public class CallToAction : TreeNode
{
    public bool HasImage { ... }
    public string ImagePath { ... }
    public string ImageAltText { ... }
}
Enter fullscreen mode Exit fullscreen mode

This Page Type has some optional content - the "Image" - which is represented by the three properties in the CallToAction class.

If CallToAction.HasImage is true, then we expect ImagePath and ImageAltText to have values, if it is false, then we will ignore the values and act as though we have no Image for our Call To Action.

Now, let's look again at our HomeController where we were trying to work with our Call To Action:

public class HomeController : Controller
{
    private readonly IPageRetriever pageRetriever;
    private readonly IPageDataContextRetriever contextRetriever;

    // ... Constructor

    public ActionResult Index()
    {
        var home = contextRetriever.Retrieve().Page;

        var cta = retriever.Retrieve<CallToAction>(
            q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
            .FirstOrDefault();

        var viewModel = new HomeViewModel
        {
            Title = home.Fields.Title,
            Image = cta.HasImage
                ? new ImageViewModel
                  { 
                      Path = cta.ImagePath,
                      AltText = cta.AltText
                  }
              : null
        };

        return View(viewModel);
    }
}
Enter fullscreen mode Exit fullscreen mode

We use a property, CTAPage, on our Home Page Type to find our Call To Action and then create our HomeViewModel with an optional Image:

public class HomeViewModel
{
    public string Title { get; set; }
    public ImageViewModel? Image { get; set; }
}

public class ImageViewModel
{
    public string Path { get; set; }
    public string AltText { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Since HomeViewModel.Image is nullable, our code models the true nature of this content πŸ‘πŸ½ - it is optional and might not exist.

When we go and render our HomeViewModel in the Razor View, we can ensure we account for this potentially missing content:

@model HomeViewModel

<h1>@Model.Title</h1>

@if (Model.Image is not null)
{
    <img src="@(Model.Image.Path)" alt="@(Model.Image.AltText)" />
}
Enter fullscreen mode Exit fullscreen mode

And with that, we've ensured that our code models the content accurately which means we are handling the business requirements and writing a more robust application πŸ’ͺ🏾.

πŸ•³ The Problems with Null

What Does Null Really Mean?

Using null to model missing data is a great first step, but its a slightly awkward πŸ™ƒ tool sometimes because the C# that represents our content model isn't the only place we will find null in our codebase. This leads us to the following questions:

  • In our code, where is the value null found and what does it mean?
  • If I'm a new developer coming into this code base and I see a method returning a nullable value, do null results imply something I should expect? (Should I log/display an error or not?)
  • Is null a normal value that represents the content model (intentionally missing) or is it the result of bad data (unintentionally missing)?

When we think about the core types of C# as a language, null is not that different from int, bool, and string - they're all primitives and have no inherit meaning to the business concerns of an application.

Relying on too many primitive types to represent our content model could be considered a case of primitive obession.

The only thing that null tells us is 'There's nothing here' but it doesn't answer the question of 'why?' there's nothing.

Null is a Special Case

As we start to use nullable reference types in our code, we'll see that each time we come across a null value, we have to treat it as a special case. This is a good thing and helps us prevent NullReferenceExceptions at runtime. At the same time, our code becomes cluttered with these 'special cases'.

An example of this can be seen using the code that populates our HomeViewModel:

var viewModel = new HomeViewModel
{
    Title = home.Fields.Title,
    Image = cta.HasImage
        ? new ImageViewModel
        {
            Path = cta.ImagePath,
            AltText = cta.AltText
        }
        : null
};
Enter fullscreen mode Exit fullscreen mode

We now have a HomeViewModel with an Image that is potentially null.

If we want to set a default AltText we need to make sure we don't cause a NullReferenceException, so we guard πŸ›‘ against that condition:

if (viewModel.Image is object && 
    string.IsNullOrWhiteSpace(viewModel.Image.AltText))
{
    viewModel.Image.AltText = "An image";
}
Enter fullscreen mode Exit fullscreen mode

Any time we want to perform some operations on the nullable property, we need to first ensure its not null, and for more complex View Models it's not hard to imagine examples with lots and lots of checks like this.

We know this is a problem, but what's its cause πŸ€·πŸΌβ€β™€οΈ?

Nullable Reference Types are Unions Types

The core issue is that null and ImageViewModel are two completely different types and using nullable reference types is really a way of creating a new type that the C# compiler can reason about. That new type is ImageViewModel or null (sometimes written as ImageViewModel | null). This is called a Tagged Union Type 🧐.

Since these two types have different properties and methods (different 'shapes'), namely that null has no properties or methods, we have to check first to see which type we're actually working with.

Talking about nullable reference types this way shows how glaring of a problem null was in C# before nullable reference types were added. The C# compiler treated the two types ImageViewModel and ImageViewModel | null as the exact same type, despite the problems we've just shown this can cause! Ooof! πŸ€¦πŸ»β€β™‚οΈ

What we'd like to do is change that ImageViewModel | null type back into a plain old ImageViewModel, so its not as complex to work with, but also handle scenarios where the content is missing πŸ€”.

🧩 The Null Object Pattern

Fortunately for us, there's a design pattern that does exactly what we are looking for, and it's called the Null Object Pattern.

The Null Object Pattern lets us get rid of null, using the custom type we've already defined, while also handling the scenarios where we have no data to populate an instance of this type πŸ˜€.

Creating a 'Null' ImageViewModel

Looking at our ImageViewModel, we could implement the Null Object as follows:


public record ImageViewModel(string Path, string AltText)
{
    public static ImageViewModel NullImage { get; } = 
        new ImageViewModel("", "");

    public bool IsEmpy => this == NullImage;
    public bool IsNotEmpty => this != NullImage;
}
Enter fullscreen mode Exit fullscreen mode

First, we change from a C# class to a C# 9.0 record (which you can read about in the Microsoft Docs) for reasons that will soon become apparent.

We also add a public static property that is read-only (it only has a getter) and name it NullImage. This property is the same type as the enclosing class (ImageViewModel), which means anywhere we need an ImageViewModel we can use NullImage.

NullImage also has some default values for its properties, which means any interactions with it will behave how we would expect (it's not dangerous to operate on the way null is πŸ˜‰).

This is a key point of the Null Object Pattern - we want to represent the null special case using our type, which gets rid of the null, and we also want our null case to behave the same as all other cases so we don't have to guard against those scenarios everywhere πŸ˜….

Notice that by making the NullImage static and read-only, it's effectively a Singleton. This is another powerful feature because it lets us check if any ImageViewModel instance is the NullImage by performing a comparison.

With C# record types, equality of objects is defined by the equality of the values in the those objects, not by the references to a spot in memory, like with C# classes πŸ€“. This means any ImageViewModel created with an empty string for both the Path and AltText will be 'equal' to the NullImage.

var house = new ImageViewModel("/path/to/image.jpg", "A House");

Console.WriteLine(house == ImageViewModel.NullImage); // False

var emptyImage = new ImageViewModel("", "");

Console.WriteLine(emptyImage == ImageViewModel.NullImage); // True
Enter fullscreen mode Exit fullscreen mode

Finally we create some convenience properties, IsEmpty and IsNotEmpty, on the ImageViewModel which compare the current instance to NullImage.

If we need to modify the Title or AltText of objects we've created, we can use the C# record with syntax, which lets us clone an object into a new one while also selectively updating values 🧐:

var image = new ImageViewModel("/path/to/image.jpg", "A");

var imageUpdated = image with { AltText = "A House" };

Console.WriteLine(image.AltText); // A
Console.WriteLine(imageUpdated.AltText); // A House
Enter fullscreen mode Exit fullscreen mode

There's lots of different ways to write the ImageViewModel and add a "Null" object - inheretance with virtual methods, private backing fields - but those details are specific to the business domain, not the Null Object Pattern itself πŸ‘πŸΏ.

Using our 'Null' ImageViewModel

Now let's use our updated ImageViewModel in the HomeController:

public class HomeController : Controller
{
    private readonly IPageRetriever pageRetriever;
    private readonly IPageDataContextRetriever contextRetriever;

    // ... Constructor

    public ActionResult Index()
    {
        var home = contextRetriever.Retrieve().Page;

        var cta = retriever.Retrieve<CallToAction>(
            q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
            .FirstOrDefault();

        var viewModel = new HomeViewModel
        {
            Title = home.Fields.Title,
            Image = cta.HasImage
                ? new ImageViewModel(cta.ImagePath, cta.AltText)
                : ImageViewModel.NullImage
        };

        if (string.IsNullOrWhiteSpace(viewModel.Image.AltText))
        {
            viewModel.Image = viewModel.Image with 
            { 
                AltText = "An Image" 
            };
        }

        return View(viewModel);
    }
}
Enter fullscreen mode Exit fullscreen mode

We still have a conditional (as a ternary) checking if the Call To Action has an image, but we don't need to special case any interactions with the HomeViewModel.Image because in the case of missing content, we still have an instance of the ImageViewModel to work with 😎.

Our Razor View can be updated as well to use the IsNotEmpty property:

@model HomeViewModel

<h1>@Model.Title</h1>

@if (Model.Image.IsNotEmpty)
{
    <img src="@(Model.Image.Path)" alt="@(Model.Image.AltText)" />
}
Enter fullscreen mode Exit fullscreen mode

By updating our code to model missing content in the ImageViewModel type itself, we remove the need to guard against NullReferenceExceptions and we can still conditionally render the content if it was intentionally missing from the Page Type data.

πŸ” Where the Null Object Pattern Falls Short

I'm a big fan of the Null Object Pattern because it encodes business logic into our types and classes, modeling our special cases but in a way that makes our code more robust and readable πŸŽ‰.

However, there are still some places where this pattern isn't ideal πŸ˜‘:

  • We have to remember to define the "Null" version of each type and ensure they have consistent naming (ex NullImage) across the application.
  • The convenience methods IsEmpty and IsNotEmpty also need to be implemented. This could be done with a base class but then we need to remember to inherit from it.
  • Picking default values for string properties is pretty easy, but what about other types? Is 0 always the best value for int properties? What about false for bool?
    • Remember, we'd like to treat our "Null" case and normal case the same in as much of our code as possible.
    • Nested objects can get especially verbose since we'll want to continue our Null Object Pattern for all of those types as well.
  • The ImageViewModel has started to become more complex because it handles both the modeling of the content and the special case of missing content.

The overarching issue is that we've pushed the 'missing content' problem into the class, giving it more responsibility and complexity πŸ˜”.

🀨 Conclusion?

Null reference types are a great way of exposing the hidden β›… null values that lurk within our code, bringing them out into the light of day 🌞 using the C# type system.

They are our first step in battling the dreaded NullReferenceException and modeling code to match our content!

However, null isn't a very ergonomic type to work with and doesn't represent our content very well - all it says is "There's nothing here".

The Null Object Pattern lets us get rid of null completely by representing the 'missing content' in our classes, treating it as a special case that behaves the same as the normal cases.

Unfortunately, the Null Object Pattern comes along with a bit of baggage that could be hard to maintain in larger applications.

Nullable reference types and the Null Object Pattern are great tools to have in our toolbox, but as we will see there are even more ways to model missing content in our Kentico Xperience applications.

In my next post on modeling missing data, we'll see how we can use a simple functional πŸ‘¨πŸ½β€πŸ”¬ programming pattern called the 'Maybe' (or 'Option') to pull the representation of missing content out of our class, which simplifies it, while still avoiding a Union Type with null 😎.

As always, thanks for reading πŸ™!

References


Photo by Jordan Madrid on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

Or my Kentico Xperience blog series, like:

Discussion (0)