Refactoring techniqueSimplifying Conditional Expressions
Introduce Null Object
Replace repeated null checks for a missing collaborator with an object that implements the same interface and does the safe default.
- Also known as
- Null Object
Problem
Many callers check whether an optional collaborator exists before using it, and each supplies the same fallback: skip the call, use zero, use "Guest". The checks are copied across the codebase, and one missing check is a crash.
Solution
Create a class that implements the collaborator's interface with neutral behavior, and return or inject it wherever null was used. Callers then call methods without checking.
Before
interface Analytics {
track(event: string, props: Record<string, unknown>): void;
flush(): Promise<void>;
}
class CheckoutService {
constructor(private analytics: Analytics | null) {}
async complete(order: Order): Promise<void> {
await this.payments.capture(order);
if (this.analytics) {
this.analytics.track("checkout_completed", { total: order.total });
}
await this.emails.sendReceipt(order);
if (this.analytics) {
await this.analytics.flush();
}
}
}After
class NoopAnalytics implements Analytics {
track(): void {}
async flush(): Promise<void> {}
}
class CheckoutService {
constructor(private analytics: Analytics = new NoopAnalytics()) {}
async complete(order: Order): Promise<void> {
await this.payments.capture(order);
this.analytics.track("checkout_completed", { total: order.total });
await this.emails.sendReceipt(order);
await this.analytics.flush();
}
}Why
- The default behavior is defined once, inside the null object, instead of at every call site.
- Callers become straight-line code.
- Tests can pass the null object explicitly to switch off a dependency.
Steps
- Confirm that callers handle the missing case the same way everywhere. Note the fallback value or action.
- Create the null class implementing the same interface, with methods that return the fallback. Run the tests.
- Change the source of the value (constructor default, factory, repository lookup) to return the null object instead of
null. Keep the old checks in place; they now see a non-null object and still work. Run the tests. - Remove the checks one call site at a time. Run the tests after each.
- If some caller needs to know the object is "missing", add an
isNull()method or keep that caller's check against the type, but try to avoid it.
Watch out
- Do not use a null object when absence is an error. A missing payment gateway should fail loudly; a silent no-op hides misconfiguration.
- Do not use it when callers react differently to absence. Then the null object cannot pick one right default, and an
Option/undefinedwith explicit handling is clearer. - In TypeScript, Kotlin, Swift or C#, optional chaining (
analytics?.track(...)) removes most of the noise for a few call sites. Use a null object when the fallback is more than "do nothing" or appears in many places. - Make null objects stateless and shareable, or each instance quietly accumulates data.
- No inverse technique exists; to undo, reinstate
nulland the checks.
Related
- Fixes Switch Statements in their
if (x == null)form, and Duplicate Code in repeated fallbacks. - A special case of Replace Conditional with Polymorphism.
- A null object is often a do-nothing Strategy.
- When absence must never happen, use Introduce Assertion instead.
Agent checklist
- Every replaced null check used the same fallback.
- Absence is a legitimate state, not a configuration error.
- The null object implements the full interface with neutral return values.
- Remaining
nullreturns from the source are gone, and tests pass.
Treats these smells
See also
- Replace Conditional with PolymorphismReplace a switch on an object's type or kind with one class per case that overrides a shared method, so new cases add code instead of editing it.
- Introduce AssertionState an assumption the code already relies on as an explicit check that fails fast, instead of leaving it in a comment or in the reader's head.
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.