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 7One 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 7run(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.