Referbruv!


Card image cap

Exploring Design Patterns - Using NullObject Pattern in ASP.NET Core

Design Patterns  • Posted 5 months ago

Nulls are one of the most common occurences when developing application logic, or when dealing with entities which can have a possibility of having no referenced value. A Null Object implies to no referenced value or a neutral behaviour. While this is no uncommon, but direct references to null objects can result in "code leakage" or "broken ends", where our application program might have a chance to break. A NullObject Pattern is one of the many design patterns which specifically explains about avoiding such situations.

Why direct usage of Nulls can be harmful:

Let's take our example of ReaderFactory class demonstrated while understanding how a Factory pattern works:

public class ReaderFactory : IReaderFactory
{
    public IReader Create(int tierId)
    {
        IReader reader = null;

        if (tierId == 1)
        {
            reader = new TierOneReader();
        }
        else if (tierId == 2)
        {
            reader = new TierTwoReader();
        }
        else if (tierId == 3)
        {
            reader = new TierThreeReader();
        }
        else if (tierId == 4)
        {
            reader = new TierFourReader();
        }

        return reader;
    }
}

The method Create() returns a reference of type IReader derived by many sub types basing on an input selection constraint. Now if we observe, there is an elseif ladder without a possible else statement for the same. Let's put it this way, if there is a scenario wherein the passed input is not matched by any of the if conditions, the return value of reader will be a NULL. Now that's not harmful if we just look at it at this level. Let's look at the client (the caller) implementation where the ReaderFactory method is invoked.

    [Route("{tierId}")]
    public IEnumerable<Reader> Read(int tierId)
    {
        var reader = factory.Create(tierId);
        return reader.ReadContent();
    }

At this level, the reader variable has no idea of what is returned by the method Create() of the ReaderFactory when the passed input parameter tierId has no matching condition branch. The resultant would be a NULL and the code breaks here leaving a NullReference Exception. Now this can be avoided by a NULL check at this point, such as below:

    [Route("{tierId}")]
    public IEnumerable<Reader> Read(int tierId)
    {
        var reader = factory.Create(tierId);
        
        if(reader != null)
            return reader.ReadContent();
        
        return new List<Reader>();
    }

Well this approach is what most of the developers do everyday, but this is not so elegant plus the method Read() is doing a job for which it is not responsible: a violation of the Single Responsibility Principle. Also, the if condition reader != null is against the Liskov Substitution Principle which implies that reader can be compared to against only a suitable type of IReader rather than a NULL which has no meaning.

"We try to encapsulate the absence of an object reference by providing an alternative which offers a suitable default behavior as expected."

The Null Alternative:

Getting back to the above scenario, a simple solution would be to entrust this NULL check activity to the class which is responsible for its instantiation: the ReaderFactory. Let there be a NULL implementation for the base type IReader, and when a NULL scenario occurs, let that implementation act with a suitable action which is expected.

// NoReader is an implementation of IReader 
// which comes into play when there's a need to use Null
public class NoReader : IReader
{
    public IEnumerable<Reader> ReadContent()
    {
        return new List<Reader>();
    }
}

Now the ReaderFactory class can be altered to have an else statement which returns an instance of NoReader when no matching Reader condition branch is found.

public class ReaderFactory : IReaderFactory
{
    public IReader Create(int tierId)
    {
        IReader reader = null;

        if (tierId == 1)
        {
            reader = new TierOneReader();
        }
        else if (tierId == 2)
        {
            reader = new TierTwoReader();
        }
        else if (tierId == 3)
        {
            reader = new TierThreeReader();
        }
        else if (tierId == 4)
        {
            reader = new TierFourReader();
        }
        else 
        {
            return new NoReader();
        }
        
        return reader;
    }
}

The ReaderController.Read() method, which is the actual client in this case, is now free of having a Null check and can be simply unvary of the object returned. '

    [Route("{tierId}")]
    public IEnumerable<Reader> Read(int tierId)
    {
        IReader reader = factory.Create(tierId);
        return reader.ReadContent();
    }

In this way we can avoid Null object checks and code leakage in our application logic by using a simple NullObject pattern.

Published 5 months ago

Sponsored Links
We use cookies to improve user experience. Learn More