Design Patterns Null Object

The Null Object pattern replaces null references with a real object that does nothing. Callers use the object like any other and skip the endless "is it null?" checks. Code becomes shorter, and a whole family of crashes disappears.

The Real-World Analogy

A hotel room can hold a "Do Not Disturb" sign or an empty hook. Staff who find an empty hook never need special rules for "no sign." A blank placeholder card that says "nothing requested" lets staff follow one routine in every room.

Problem Without Null Object

Logger logger = findLogger(name);     // may return null

if (logger != null) {
    logger.log("Saved");
}
if (logger != null) {
    logger.log("Closed");
}

Every caller repeats the check. One forgotten check crashes the program.

Solution With Null Object

              ┌──────────────────┐
              │ «interface»      │
              │ Logger           │
              │ + log(message)   │
              └────────▲─────────┘
            ┌──────────┴───────────┐
   ┌────────┴────────┐    ┌────────┴────────┐
   │ FileLogger      │    │ NullLogger      │
   │ writes to disk  │    │ does nothing    │
   └─────────────────┘    └─────────────────┘

findLogger() returns NullLogger instead of null.
Callers invoke log() without checks.

Structure

Abstract Type

Declares the operations that clients call.

Real Object

Performs the actual work.

Null Object

Implements every operation with an empty or neutral result. Methods that return numbers give zero, methods that return text give an empty string, and methods that return lists give an empty list.

Code Example

interface Logger {
    void log(String message);
}

class FileLogger implements Logger {
    public void log(String message) {
        writeToFile(message);
    }
}

class NullLogger implements Logger {
    public void log(String message) {
        // intentionally empty
    }
}

class LoggerFactory {
    static Logger find(String name) {
        if (exists(name)) {
            return new FileLogger();
        }
        return new NullLogger();       // never returns null
    }
}

Logger logger = LoggerFactory.find("payments");
logger.log("Saved");       // safe, no check needed
logger.log("Closed");

Before and After Comparison

BEFORE                              AFTER
─────────────────────────────       ─────────────────────────────
if (customer != null)               String plan = customer.getPlan();
    plan = customer.getPlan();
else
    plan = "Free";                  (GuestCustomer returns "Free")

A GuestCustomer class stands in when no customer exists. Its getPlan method returns "Free," and the caller never branches.

Real Projects Using Null Object

ScenarioNull Version
LoggingLogger that discards messages
User profileGuest user with default settings
Discount rulesZero-percent discount
Event listenersListener that ignores events

Pitfalls to Watch

  • Silent failures can hide real problems. Log a note or use a distinct type when the missing value signals an error.
  • Equality checks against the null object need care, because code may need to detect "nothing here."
  • Too many null classes clutter a project. Create one only when callers repeat the same check often.

Advantages and Disadvantages

AdvantagesDisadvantages
Removes repetitive null checksMay mask genuine errors
Prevents null-reference crashesAdds one more class per type
Uses polymorphism instead of branchingDebugging "nothing happened" takes thought

Step-by-Step Walkthrough

Follow the logger example. A caller asks LoggerFactory for a logger named "payments". The factory looks for a matching configuration. A match returns a FileLogger, and a miss returns a NullLogger. The caller invokes log() in both cases and never inspects the result.

In the miss case, the call does nothing. The program keeps running, and no crash occurs.

Choosing Neutral Results

  • Numbers: zero or another harmless default.
  • Text: an empty string or a friendly label such as "Guest".
  • Lists: an empty list that loops safely.
  • Actions: an empty method body.

Common Mistakes

  • Hiding an error that users should see, such as a missing payment method.
  • Returning null from some paths and a null object from others. Pick one style and apply it everywhere.
  • Creating a new null object on every call. A single shared instance saves memory.

Key Takeaways

  • A null object offers safe do-nothing behavior behind the normal interface.
  • Factories and finders return it in place of null.
  • Callers skip checks and trust polymorphism.
  • Use it for optional collaborators, and avoid it when missing data needs loud errors.