Home Design Patterns Null Object

Null Object

Modern Practice

Supply a do-nothing implementation so callers never have to test for absence.

Purpose

Some collaborators are optional. There may be no logger, no progress reporter, no audit sink. The usual way to say that is to hold a null reference and test it before every use, which means the same three-line guard repeated at every call site and one crash the day somebody forgets. A Null Object replaces the null with a real object of the right type whose methods do nothing, so there is nothing to test for.

The important word is neutral. A Null Object is not "an object that will not crash", it is an object whose behavior is the correct behavior for the absent case: writing to a log that is switched off really is a no-op, and asking an empty collection for its contents really does yield nothing. Where doing nothing is not obviously right, the pattern is the wrong tool and is actively dangerous, which is the theme of the rest of this page.

Design

The null object implements the same interface as the real thing. Void methods return immediately. Methods that must return a value return the neutral value for that type: an empty collection, a zero, an identity, or another null object. Because it holds no state and every instance behaves identically, one shared immutable instance is enough, and exposing it as a constant is conventional.

UML class diagram: Client depends on the Logger interface; FileLogger and NullLogger both implement it, and NullLogger's methods do nothing.
Nothing in the picture is unusual: it is an interface with two implementations. The pattern is entirely in what the second one does, which is why it cannot be recognized from a diagram alone.

The decision that matters is where the null object is chosen. It should be selected once, deliberately, at the point where the program is assembled, and handed in by Dependency Injection. What it must never be is a silent fallback for a failure, because at that moment the pattern stops expressing "this feature is off" and starts expressing "something went wrong and I decided not to mention it".

Example

The Autobot Robotics embedded controller writes a motion trace: every commanded move, every limit trip, every fault code. On a bench machine that goes to a file. On a sealed controller in a wash-down food plant there is no writable filesystem and tracing is switched off entirely. The code was full of if (trace != null) trace.write(...), and one path in the fault handler did not have the guard. It had been there for two years without being reached, and then a customer configuration reached it: a null dereference inside the fault handler, which took down the motion loop while the arm was recovering from an earlier fault. The worst possible place to crash is the code that runs when something has already gone wrong.

They added a NullTraceLog whose write returns immediately, made it the value used when tracing is disabled, and deleted every guard. Around forty null checks came out of the controller, and the fault handler became unconditional, which had a second benefit specific to real-time work: the code path is now identical whether tracing is on or off, so the timing measured on the bench is the timing that happens on site.

Then it bit them. A site reported that an Arm1210 was intermittently dropping jobs, and there were no traces at all to look at. The log path in that site's configuration pointed at a directory that no longer existed, the open failed, and the start-up code had helpfully fallen back to NullTraceLog. The system had been running for five weeks in a state everyone believed was impossible. The fix was one line and worth more than the pattern: failing to open the log the operator asked for is now a start-up error, and the null log is used only when the configuration explicitly says tracing is off.

interface TraceLog
{
    void write(string msg);
    string[] recent(int n);
}

class FileTraceLog : TraceLog
{
    public void write(string msg) { /* append to the open file */ }
    public string[] recent(int n) { /* read back the tail */ }
}

// Every method does nothing, and returns the neutral value for its type.
class NullTraceLog : TraceLog
{
    public static readonly NullTraceLog Instance = new NullTraceLog();

    public void write(string msg) { }
    public string[] recent(int n) { return new string[0]; }
}

class FaultHandler
{
    private readonly TraceLog trace;

    public FaultHandler(TraceLog t) { trace = t; }

    public void onFault(int code)
    {
        trace.write("fault " + code);   // no guard, and none needed
        // stop motion, latch the code, signal the operator
    }
}

class ControllerStartup
{
    public TraceLog openTrace(Config cfg)
    {
        // Off is a decision. A failure to open is NOT the same thing,
        // and must not quietly become one.
        if (cfg.traceDisabled) return NullTraceLog.Instance;
        return new FileTraceLog(cfg.tracePath);
    }
}
Two lines carry the pattern: the empty body of write and the unguarded call in onFault. The third line worth staring at is the if in openTrace, which is what keeps the null object from becoming a way to hide a broken configuration.

Seen in the Wild

The .NET base class library ships two textbook examples as static fields: Stream.Null and TextWriter.Null both accept everything written to them and discard it. Python's standard library documents logging.NullHandler as the thing a library should attach to its logger so that an application which has not configured logging gets silence rather than warnings. SLF4J does the same with NOPLogger when no binding is present. Java's Collections.emptyList is the collection form of the same idea: a real list you can iterate, with nothing in it. Below all of these sits /dev/null, which is the pattern implemented as a device file and predates the vocabulary by decades.

Comparisons

  • Strategy is the closest relative: a Null Object is the do-nothing member of a family of interchangeable behaviors, and it is the cleanest way to express "no special handling" without a conditional in the context.
  • Singleton and Flyweight both apply, because a null object holds no state. One immutable instance can be shared by every caller in the process, which is the rare case where a global instance is genuinely harmless.
  • Proxy also stands in for something else, but a proxy is a stand-in that eventually reaches the real subject. A null object never reaches anything; there is nothing behind it.
  • Dependency Injection is how it should arrive. A null object chosen inside the class that uses it is just a conditional in disguise.
  • State uses the same trick for the initial or terminal state of a machine, where a state that accepts every event and does nothing is often exactly right.

Why It Exists

My take

Null Object exists because a null reference is not a value, it is the absence of one, and languages that let you hold absence in a variable of a real type force every user of that variable to handle a case the type system claims cannot happen. Tony Hoare called this his billion dollar mistake and he was talking about exactly this: the check is required, the type does not require it, and so the check gets missed. A null object is a way of saying "absent" using a value instead of the absence of one, which puts the case back inside the type.

What it buys is not shorter code, it is the removal of a decision from every call site. Forty guards in a controller are forty places where somebody has to know what "no trace log" means, and one of them will get it wrong, usually the one written at four in the afternoon in the error path that nobody exercises. Moving the decision to one class means the answer is written once, has a name, and can be reviewed.

My reservation is sharper than for most patterns, and it is this: a null object hides errors just as effectively as it hides absence, and it cannot tell the two apart. A do-nothing logger that was selected because logging is off and a do-nothing logger that was selected because the log file could not be opened are the same object, and the second one has converted a loud failure into five weeks of silence. Every do-nothing implementation I have regretted was regretted for that reason. The rule I now apply is that a null object may only be chosen by an explicit decision that names the feature as off. It may never be a catch block's idea of a sensible default.

I would also resist it for anything that returns data rather than absorbing it. A null repository that returns an empty list makes "there are no matching jobs" and "the database is unreachable" indistinguishable to the caller, and the caller will draw the wrong conclusion and act on it. Absorbing output is a safe place for this pattern. Manufacturing input is not.

Criticisms

The failure mode is silence, and silence is the hardest kind of bug to notice. A crash produces a stack trace that points at a line. A null object produces a system that runs, reports success, and does not do the thing. The gap between the bug being introduced and the bug being noticed is measured in weeks rather than seconds, and the discovery usually happens during an incident where the missing behavior was exactly what you needed.

It also does not scale to wide interfaces. Every method needs a neutral implementation, and for methods that return values the neutral answer is often not obvious. What does a null Customer return from creditLimit()? Zero is a decision with consequences, and whatever you pick, callers will start relying on it, at which point the null object is not neutral any more, it is a business rule stored in the least likely file. Add a method to the interface later and every null object needs another judgment call.

Finally it can obscure a design that should have been explicit. If half the system genuinely does not need a collaborator, the honest answer may be that the collaborator does not belong in that half, rather than that it should be present and inert. And in a language with a real option type, or with nullable reference types switched on, the compiler will now force the checks the pattern was invented to eliminate, which makes a plain Option the better tool for absence and leaves Null Object for the narrower case where doing nothing is a named, intended behavior.