Replace Type Code with Subclasses
Intent
Section titled “Intent”Take a field that holds a type code — an enum or string like "engineer", "manager", "sales" — that drives conditional behaviour, and replace each branch with a subclass. The shared base declares the operation; each subclass supplies its own version. The switch vanishes, and adding a new variant means adding a class instead of editing every conditional.
The smell
Section titled “The smell”The smell is a type code paired with switching logic: one field whose value is repeatedly inspected by switch or chained if statements that pick behaviour. The trouble compounds. The same switch over the same code tends to appear in several methods, so every new type means editing each of them, and the compiler offers no help when you forget one. Polymorphism turns those scattered branches into a single dispatch the language performs for you.
Before → After
Section titled “Before → After”An Employee whose pay depends on a type string checked by a switch. After, each employee type is a subclass that knows its own pay rule.
// Beforeclass Employee { constructor(public type: string, public base: number) {} payAmount(): number { switch (this.type) { case 'engineer': return this.base; case 'manager': return this.base * 1.2; case 'sales': return this.base + 500; default: throw new Error(`unknown type ${this.type}`); } }}
// Afterabstract class Employee { constructor(public base: number) {} abstract payAmount(): number;}class Engineer extends Employee { payAmount(): number { return this.base; }}class Manager extends Employee { payAmount(): number { return this.base * 1.2; }}class Salesperson extends Employee { payAmount(): number { return this.base + 500; }}# Beforeclass Employee: def __init__(self, kind, base): self.kind = kind self.base = base def pay_amount(self): if self.kind == "engineer": return self.base elif self.kind == "manager": return self.base * 1.2 elif self.kind == "sales": return self.base + 500 raise ValueError(f"unknown type {self.kind}")
# Afterfrom abc import ABC, abstractmethod
class Employee(ABC): def __init__(self, base): self.base = base @abstractmethod def pay_amount(self): ...
class Engineer(Employee): def pay_amount(self): return self.base
class Manager(Employee): def pay_amount(self): return self.base * 1.2
class Salesperson(Employee): def pay_amount(self): return self.base + 500// Beforetype Employee struct { Kind string Base float64}func (e Employee) PayAmount() float64 { switch e.Kind { case "engineer": return e.Base case "manager": return e.Base * 1.2 case "sales": return e.Base + 500 default: panic("unknown type " + e.Kind) }}
// Aftertype Employee interface { PayAmount() float64}
type Engineer struct{ Base float64 }func (e Engineer) PayAmount() float64 { return e.Base }
type Manager struct{ Base float64 }func (m Manager) PayAmount() float64 { return m.Base * 1.2 }
type Salesperson struct{ Base float64 }func (s Salesperson) PayAmount() float64 { return s.Base + 500 }// Beforestruct Employee { kind: String, base: f64,}impl Employee { fn pay_amount(&self) -> f64 { match self.kind.as_str() { "engineer" => self.base, "manager" => self.base * 1.2, "sales" => self.base + 500.0, other => panic!("unknown type {other}"), } }}
// Aftertrait Employee { fn pay_amount(&self) -> f64;}
struct Engineer { base: f64 }impl Employee for Engineer { fn pay_amount(&self) -> f64 { self.base }}
struct Manager { base: f64 }impl Employee for Manager { fn pay_amount(&self) -> f64 { self.base * 1.2 }}
struct Salesperson { base: f64 }impl Employee for Salesperson { fn pay_amount(&self) -> f64 { self.base + 500.0 }}flowchart TD
subgraph Before["Before — a type code"]
A["Employee<br/>type: 'engineer' | 'manager' | 'sales'"]
A --> B{"switch type"}
B --> C["pay logic for engineer"]
B --> D["pay logic for manager"]
B --> E["pay logic for sales"]
end
subgraph After["After — a hierarchy"]
F["Employee (abstract)<br/>payAmount()"]
F --> G["Engineer"]
F --> H["Manager"]
F --> I["Salesperson"]
end
Before -.->|"Replace Type Code with Subclasses"| After Mechanics
Section titled “Mechanics”- If the type code is not yet encapsulated, hide it behind an accessor first, so callers do not depend on the raw field.
- Create a subclass for one value of the type code. Provide a factory (or constructor) that returns the right subclass for a given code, so creation stays in one place.
- Move the behaviour for that value — one branch of the
switch— into an overriding method on the subclass. - Run your tests.
- Repeat for each remaining type-code value, removing each branch from the original conditional as you go.
- When the last branch is gone, delete the now-empty
switchand, if nothing else needs it, the type-code field itself. The base operation is now abstract, supplied entirely by subclasses.
When to use / trade-offs
Section titled “When to use / trade-offs”Use subclasses when the type is fixed for the life of the object and when several methods branch on the same code. Polymorphism collapses those branches and lets the compiler ensure every subclass implements the operation, so a forgotten case becomes a build error rather than a runtime surprise.
But subclasses are not always the right tool. If an object’s type changes during its lifetime — an order moving from “pending” to “shipped” to “delivered” — you cannot swap its class, so reach for the State pattern: hold a separate state object the entity can replace as it transitions. And if you only need to vary one algorithm rather than a whole family of behaviour, Strategy — injecting a behaviour object — is lighter than a full hierarchy. Choose subclasses for stable identity, State for changing identity, Strategy for swappable single algorithms.