Refactoring techniqueSimplifying Conditional Expressions
Replace Conditional with Polymorphism
Replace 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.
Problem
A conditional selects behavior by checking a type code, a kind field or instanceof. The same switch, with the same cases, appears in several functions. Each new kind forces you to find and edit every one of them.
Solution
Create a class (or an object implementing a shared interface) for each case, move each branch into an overriding method on its class, and replace the conditional with a single method call.
Before
type Notification = { channel: "email" | "sms" | "push"; to: string; body: string };
function send(n: Notification): Promise<void> {
switch (n.channel) {
case "email":
return mailer.send({ to: n.to, subject: n.body.slice(0, 60), html: n.body });
case "sms":
return smsGateway.post(n.to, n.body.slice(0, 160));
case "push":
return pushService.notify(n.to, { alert: n.body });
}
}
function costCents(n: Notification): number {
switch (n.channel) {
case "email": return 0;
case "sms": return Math.ceil(n.body.length / 160) * 4;
case "push": return 0;
}
}After
interface Channel {
send(to: string, body: string): Promise<void>;
costCents(body: string): number;
}
const email: Channel = {
send: (to, body) => mailer.send({ to, subject: body.slice(0, 60), html: body }),
costCents: () => 0,
};
const sms: Channel = {
send: (to, body) => smsGateway.post(to, body.slice(0, 160)),
costCents: (body) => Math.ceil(body.length / 160) * 4,
};
const push: Channel = {
send: (to, body) => pushService.notify(to, { alert: body }),
costCents: () => 0,
};
const channels = { email, sms, push } satisfies Record<string, Channel>;
// Callers: channels[n.channel].send(n.to, n.body)Why
- Everything about SMS lives in one object. Adding WhatsApp is one new object and one map entry.
- Duplicated switches disappear, so no case can be forgotten in one of them.
- Each variant can be tested alone.
Steps
- Find all conditionals that switch on the same discriminator. If there is only one, and it is short, stop here.
- Define the interface from the methods those conditionals implement.
- Create one class or object per case. Start with all methods delegating back to the old function, so behavior is unchanged. Run the tests.
- Move one branch body into its class method. Run the tests. Repeat branch by branch.
- Add the lookup (map, factory or constructor parameter) that picks the right implementation once, at the edge.
- Replace each switch with a call through the interface. Delete the old function when it has no callers. Run the tests.
Watch out
- Do not apply this to a single switch that nothing else repeats. A switch with exhaustiveness checking (TypeScript
never, Rustmatch, Java sealed types) is clear and safe. - If operations change more often than kinds, polymorphism makes things worse: each new operation touches every class. Keep a discriminated union and pattern matching, or use Visitor.
- The switch moves, it does not vanish. Something still maps
"sms"to the SMS object; keep that in one place. - When the kind of an object changes at runtime, use Replace Type Code with State/Strategy rather than subclasses.
- No direct inverse technique exists. Collapsing polymorphic classes back into a switch is done manually with Inline Class and Collapse Hierarchy.
Related
- Fixes Switch Statements and the Shotgun Surgery that repeated switches cause.
- Prepared by Replace Type Code with Subclasses or Replace Type Code with State/Strategy.
- Results in Strategy or State.
Agent checklist
- The same discriminator is switched on in more than one place, or the single switch is long and growing.
- Kinds change more often than operations.
- The kind-to-implementation mapping exists in exactly one place.
- Each branch moved separately with tests green between moves.
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.
- Shotgun SurgeryOne logical change forces small edits in many classes, so one missed edit ships an inconsistent system.
- Long MethodA function that does several jobs in a row and has grown too long to read, name or test as one unit.
See also
- Replace Type Code with SubclassesTurn each value of an immutable type code that drives conditionals into a subclass, and move the branch logic into overrides.
- 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.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
- StateMove each mode of an object into its own class so behavior and transitions change with the mode instead of branching on a status field.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.