Introduce Special Case
Intent
Section titled “Intent”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.
The smell
Section titled “The smell”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.
Before → After
Section titled “Before → After”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.
// Beforefunction 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;}
// Afterclass 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;}# Beforedef customer_name(site): return "occupant" if site.customer is None else site.customer.name
def billing_plan(site): return "basic" if site.customer is None else site.customer.plan
# Afterclass UnknownCustomer: name = "occupant" plan = "basic" is_unknown = True
def customer_name(site): return site.customer.name
def billing_plan(site): return site.customer.plan// Beforefunc CustomerName(site Site) string { if site.Customer == nil { return "occupant" } return site.Customer.Name}
func BillingPlan(site Site) string { if site.Customer == nil { return "basic" } return site.Customer.Plan}
// Afterfunc UnknownCustomer() *Customer { return &Customer{Name: "occupant", Plan: "basic", IsUnknown: true}}
func CustomerName(site Site) string { return site.Customer.Name}
func BillingPlan(site Site) string { return site.Customer.Plan}// Beforefn customer_name(site: &Site) -> String { match &site.customer { None => "occupant".to_string(), Some(c) => c.name.clone(), }}
fn billing_plan(site: &Site) -> String { match &site.customer { None => "basic".to_string(), Some(c) => c.plan.clone(), }}
// Afterimpl Customer { fn unknown() -> Customer { Customer { name: "occupant".to_string(), plan: "basic".to_string(), is_unknown: true, } }}
fn customer_name(site: &Site) -> String { site.customer.name.clone()}
fn billing_plan(site: &Site) -> String { site.customer.plan.clone()}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 Mechanics
Section titled “Mechanics”- Add a property or method that lets callers ask whether a value is the special case (for example
isUnknown), and make the normal objects answerfalse. - Create the special-case object — a subclass, struct, or factory — that implements the same interface and returns the agreed defaults for each accessor.
- Arrange for the source of the value to hand back the special-case object instead of null when the value is absent.
- Replace each
if (x == null)site with a direct use of the value, one site at a time. Run your tests after each. - When the last explicit check is gone, remove any now-dead null-handling code.
When to use / trade-offs
Section titled “When to use / trade-offs”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.
Related
Section titled “Related”- Replace Nested Conditional with Guard Clauses
- Replace Conditional with Polymorphism
- Consolidate Conditional Expression