Skip to main content

Primitive Obsession, Switch Statements, Speculative Generality

What This Concept Is

Three smells that cluster around type discipline and future-proofing regret:

  • Primitive Obsession. Using built-in types (String, int, double) for domain concepts that deserve their own type -- money as double, telephone numbers as String, dates as long epochs. The domain loses its vocabulary; calculations lose their units; invalid values slip in undetected.
  • Repeated Switch. The same switch/if-else on a type code shows up in several places. Every new case requires finding and updating every switch; missing one silently breaks behavior.
  • Speculative Generality. Abstract classes, interfaces, extension points, and hooks introduced "just in case" a second variant shows up -- which it never does. The code ships with ceremony that delivers no value today and carries maintenance cost forever.

The first two pull toward more type or polymorphism. The third pulls away from it. Taken together they mark the boundary where you should abstract, and where you should stop.

Why It Matters Here

These smells are where the SOLID cluster meets reality. Primitive Obsession and Repeated Switch are the classic triggers for Replace Primitive with Object, Replace Type Code with Subclasses, and Replace Conditional with Polymorphism. Speculative Generality is the check that prevents those moves from turning into ceremony.

If you only learn "always use polymorphism," you will over-abstract. If you only learn "YAGNI," you will under-abstract. You need both, and the smell set tells you which pressure applies.

Concrete Example

Primitive Obsession:

def charge(card_number: str, amount: float, currency: str):
...

Currency is a string; amount is a float. Adding dollars and euros silently typechecks. Real domain types remove the hazard:

@dataclass(frozen=True)
class Money:
amount: Decimal
currency: Currency
def plus(self, other: "Money") -> "Money":
assert self.currency == other.currency
return Money(self.amount + other.amount, self.currency)

def charge(card: Card, total: Money): ...

Repeated Switch:

switch (shape.kind) {
case CIRCLE: return Math.PI * shape.r * shape.r;
case RECTANGLE: return shape.w * shape.h;
}
// ... same switch appears in draw(), boundingBox(), contains()

Each new shape kind requires edits in N places -- exactly the OCP violation. Replace with polymorphism: each shape knows its area(), draw(), boundingBox(), contains().

Speculative Generality:

public abstract class AbstractProcessor<T, R, E extends ProcessorEvent> {
protected abstract R doProcess(T t) throws E;
protected void beforeHook(T t) {}
protected void afterHook(R r) {}
}
public class OrderProcessor extends AbstractProcessor<Order, Receipt, OrderEvent> { ... }

Two type parameters, generic exception, two hook methods -- and exactly one subclass. None of the flexibility is used. Collapse to a concrete class until a second, real use case shows up.

Common Confusion / Misconception

"All switches are bad." Not true. A switch over a stable, closed set (e.g., day of week) is fine. The smell is the repeated switch -- the same discrimination in many places. A single switch in one dispatcher is routine; three switches on the same type across the codebase is a signal.

A second misconception: "creating a class for every primitive is always good." Wrap primitives when the domain gives them meaning (Money, Address, PhoneNumber). Wrapping a primitive just to introduce a layer (UserId that's literally int) can add noise in languages without cheap newtype support.

A third: "more abstraction always helps." Speculative Generality is the counterweight. If the second variant never arrives, the abstraction is pure cost.

How To Use It

When you find one of these smells, apply the canonical move:

  1. Primitive Obsession -> Replace Primitive with Object. Create a domain type; put validation, formatting, and arithmetic on it.
  2. Repeated Switch -> Replace Conditional with Polymorphism. Introduce a family of subclasses or strategies; each case moves to its class.
  3. Speculative Generality -> Inline Class / Remove Hook / Collapse Hierarchy. Delete the layer until a second real use case demands it.

Check Yourself

  1. Why is a single switch different from a repeated switch?
  2. Why does wrapping a String phone number in a PhoneNumber class add value that a typedef does not?
  3. How can Speculative Generality coexist with the Open-Closed Principle without contradicting it?

Mini Drill or Application

Pick a real file with at least one switch or type flag. Do all four:

  1. Count the places the same switch appears. One -> fine. Two or more -> candidate for polymorphism.
  2. List any primitive fields that represent a domain concept (money, phone, email, id).
  3. For one of them, sketch the object you would introduce and which existing code would become simpler.
  4. Look for any abstract class or interface with exactly one implementation -- mark it as a Speculative Generality candidate.

Read This Only If Stuck