A logbook for curious waters
← Software Engineering Field Guide

Introduce Null Object

Before you begin: Objects, interfaces or compatible methods, and null checks.

Introduce Null Object replaces repeated checks for a missing collaborator with an object that performs the intended “nothing to do” behavior. The object still satisfies the operation’s contract; its emptiness is deliberate rather than an accidental missing dependency. It is a form of Special Case. 1

Original version: optional diagnostic logging

A small job can run with or without diagnostic messages. Logging is genuinely optional here; this is not a security audit trail or a required payment record.

public static class Job
{
    public static int Run(Action<string>? log)
    {
        if (log != null) log("started");
        return 7;
    }
}
def run(log):
    if log is not None:
        log("started")
    return 7

One null check is harmless. If many operations repeat it, the absence rule becomes scattered across callers.

Refactored version: a logger that intentionally stays silent

public interface ILog
{
    void Write(string message);
}
public sealed class RecordingLog : ILog
{
    public List<string> Messages { get; } = new List<string>();
    public void Write(string message) { Messages.Add(message); }
}
public sealed class SilentLog : ILog
{
    public void Write(string message) { }
}
public static class Job
{
    public static int Run(ILog log)
    {
        log.Write("started");
        return 7;
    }
}

Job.Run(new SilentLog()) returns 7 without recording a message. Run no longer distinguishes absence; its caller supplies a logger whose behavior is silence.

class RecordingLog:
    def __init__(self):
        self.messages = []

    def write(self, message):
        self.messages.append(message)


class SilentLog:
    def write(self, message):
        pass


def run(log):
    log.write("started")
    return 7

run(SilentLog()) performs the job with no diagnostic message. The recording implementation provides the same method but retains the message for inspection.

Absence is not always success

A silent diagnostic logger is plausible. A “null payment processor” that pretends a charge succeeded is not an equivalent fallback for a missing payment service. Decide whether absence should mean a default, a no-op, an explicit missing result, or an error.

The revised API assumes a non-null compatible logger. Passing null directly now fails; construction code must translate intentional absence into SilentLog. That migration is part of the change.

Also check argument evaluation. With a null check, an expensive message expression might never run. An unconditional call to a silent object still evaluates ordinary arguments before calling it. Preserve lazy message construction when that matters.

For one optional operation, a simple conditional may remain clearer than three new types. Use a Null Object when one well-defined absence behavior removes repetition without hiding a meaningful failure.

Sources

  1. Martin Fowler — Introduce Special Case.