Refactoring techniqueOrganizing Data
Replace Type Code with Subclasses
Turn each value of an immutable type code that drives conditionals into a subclass, and move the branch logic into overrides.
- Also known as
- Replace Type Code with Polymorphism
Problem
A class has a type field, and several methods switch on it. Each new type means editing every switch. Some fields only make sense for one type and sit unused for the others.
Solution
Create a subclass for each type value, move each branch into an override in its subclass, and create instances through a factory. Use this when the type of an object never changes after creation.
Before
class Notification {
constructor(
readonly channel: "email" | "sms" | "push",
readonly recipient: string,
readonly body: string,
readonly deviceToken?: string, // push only
) {}
maxLength(): number {
switch (this.channel) {
case "email": return 100_000;
case "sms": return 160;
case "push": return 240;
}
}
send(gateway: Gateway): Promise<void> {
switch (this.channel) {
case "email": return gateway.email(this.recipient, this.body);
case "sms": return gateway.sms(this.recipient, this.body.slice(0, 160));
case "push": return gateway.push(this.deviceToken!, this.body);
}
}
}After
abstract class Notification {
constructor(readonly recipient: string, readonly body: string) {}
abstract maxLength(): number;
abstract send(gateway: Gateway): Promise<void>;
static create(channel: string, recipient: string, body: string, deviceToken?: string): Notification {
switch (channel) {
case "email": return new EmailNotification(recipient, body);
case "sms": return new SmsNotification(recipient, body);
case "push": return new PushNotification(recipient, body, deviceToken!);
default: throw new Error(`unknown channel ${channel}`);
}
}
}
class EmailNotification extends Notification {
maxLength() { return 100_000; }
send(g: Gateway) { return g.email(this.recipient, this.body); }
}
class SmsNotification extends Notification {
maxLength() { return 160; }
send(g: Gateway) { return g.sms(this.recipient, this.body.slice(0, this.maxLength())); }
}
class PushNotification extends Notification {
constructor(recipient: string, body: string, readonly deviceToken: string) { super(recipient, body); }
maxLength() { return 240; }
send(g: Gateway) { return g.push(this.deviceToken, this.body); }
}Why
- Adding a channel is a new subclass plus one factory line; existing classes stay untouched.
deviceTokenexists only where it means something, and is no longer optional.- Each channel's rules can be read and tested on their own.
Steps
- Wrap construction in a factory method (
create) so callers stop calling the constructor with a type code. Run the tests. - Self-encapsulate the type field: replace direct reads with a getter.
- Create one subclass per type value. Override the getter to return its constant. Make the factory return the subclass. Run the tests.
- For each method that switches on the type, copy each branch into an override in the matching subclass. Make the base method abstract. Run the tests after each method.
- Move type-specific fields down into their subclass.
- Remove the type field from the base if nothing reads it any more.
Watch out
- Requires the type to be fixed for the object's lifetime. If an order can go from
"draft"to"paid", use Replace Type Code with State/Strategy. - Requires that the class is not already subclassed along another axis. If it is, composition (State/Strategy) avoids a combinatorial explosion.
- One switch over three stable values may be clearer than three classes. In TypeScript, a discriminated union with exhaustive
switchis often the idiomatic alternative, and the compiler checks completeness. - The factory still has a switch. That is fine; it is the one place that maps codes to types.
- The inverse is Replace Subclass with Fields.
Related
- Fixes Switch Statements, Primitive Obsession and Temporary Field.
- Inverse: Replace Subclass with Fields.
- Uses Replace Conditional with Polymorphism for each switch, and a Factory Method for creation.
Agent checklist
- The type code never changes after construction.
- The class has no other subclass hierarchy that would multiply.
- Construction goes through one factory; no caller passes type codes to constructors.
- Every switch on the type was replaced with overrides, and type-only fields moved down.
- A discriminated union was considered as a lighter alternative.
Treats these smells
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
- Primitive ObsessionDomain concepts like money, emails, IDs or statuses are passed around as raw strings and numbers, so their rules get re-checked everywhere.
- Large ClassA class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.
- Temporary FieldA field that is set and meaningful only during one operation or in one special case, and is empty or stale the rest of the time.
See also
- Replace Type Code with ClassReplace a set of string or number codes that don't affect behavior with a dedicated type, so only valid codes can be used.
- Replace Type Code with State/StrategyMove behavior that depends on a type code into separate state or strategy objects that the host holds and can swap at runtime.
- Replace Subclass with FieldsCollapse subclasses that differ only in the constants their methods return into fields on the parent, then delete the subclasses.
- 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.
- Factory MethodLet a subclass or a supplied function decide which concrete object to create, so shared workflow code depends only on an interface.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.