Skip to content

Introduce Special Case

Take a value that is sometimes missing — and a null check repeated everywhere that value is used — and replace the missing case with a real object that knows the right default answers. This special case (often a null object) implements the same interface, so callers just use it; the “what if it is absent?” branch disappears from every call site and lives in one class instead.

This is the cure for a repeated special-case check, classically if (x == null). When a field can be absent, every method that touches it grows the same guard, each substituting its own default — a name here, a plan there, a billing amount elsewhere. The knowledge of “what a missing customer means” is smeared across the codebase, and any new default has to be added in yet another place. A special-case object collects all those defaults in one type that answers normally.

Display code reads a customer’s name and plan, defaulting for unknown customers. Before, every site checks for null. After, an UnknownCustomer supplies the defaults.

// Before
function customerName(site: Site): string {
return site.customer === null ? 'occupant' : site.customer.name;
}
function billingPlan(site: Site): string {
return site.customer === null ? 'basic' : site.customer.plan;
}
// After
class UnknownCustomer implements Customer {
readonly name = 'occupant';
readonly plan = 'basic';
readonly isUnknown = true;
}
function customerName(site: Site): string {
return site.customer.name;
}
function billingPlan(site: Site): string {
return site.customer.plan;
}
flowchart LR
  subgraph Before["Before"]
    A["customer == null<br/>? 'occupant'<br/>: customer.name"]
    B["customer == null<br/>? 'basic'<br/>: customer.plan"]
  end
  subgraph After["After"]
    C["customer.name"]
    D["customer.plan"]
    E["UnknownCustomer<br/>name = 'occupant'<br/>plan = 'basic'"]
  end
  Before -.->|"Introduce Special Case"| After
Scattered null checks collapse into one special-case object with defaults
  1. Add a property or method that lets callers ask whether a value is the special case (for example isUnknown), and make the normal objects answer false.
  2. Create the special-case object — a subclass, struct, or factory — that implements the same interface and returns the agreed defaults for each accessor.
  3. Arrange for the source of the value to hand back the special-case object instead of null when the value is absent.
  4. Replace each if (x == null) site with a direct use of the value, one site at a time. Run your tests after each.
  5. When the last explicit check is gone, remove any now-dead null-handling code.

Reach for Introduce Special Case when the same missing-value check, with the same default, repeats across many call sites. Centralizing those defaults in one object removes duplication and gives the absent case a name and a home.

It is not always the right tool. If the missing value means different things in different places — default here, an error there — a single special-case object cannot serve all of them, and forcing one hides important differences. For a value checked in just one or two spots, a plain guard clause is simpler. Use the special case when the uniformity of the default is what makes it worth a type.

What does Introduce Special Case replace?
How should the special-case object behave when a caller uses it?
When is a single special-case object the WRONG choice?
What replaces each "if value is null" site after the refactoring?