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
| Scenario | Null Version |
|---|---|
| Logging | Logger that discards messages |
| User profile | Guest user with default settings |
| Discount rules | Zero-percent discount |
| Event listeners | Listener 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
| Advantages | Disadvantages |
|---|---|
| Removes repetitive null checks | May mask genuine errors |
| Prevents null-reference crashes | Adds one more class per type |
| Uses polymorphism instead of branching | Debugging "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.
