Replace Conditional with Polymorphism — Refactoring Patterns, Part 4
Updated: Jul 30
Refactoring Patterns · Part 4
When the same switch (type) on the same set of cases appears in five different functions, adding a sixth type means hunting down all five and hoping you didn't miss one. Replacing that conditional with polymorphism puts each type's behavior in one place. This walks through the refactor — and, just as important, when a plain if is the right answer and reaching for polymorphism is over-engineering.
A switch statement that keeps growing is a class hierarchy in disguise. Promote it.
Step 1: The Smell (15 min)
function calculateShipping(type, weight) {
switch (type) {
case 'standard': return weight * 0.5
case 'express': return weight * 1.5 + 10
case 'overnight': return weight * 3 + 30
}
}
Add new shipping type: edit all switches everywhere. Easy to miss one.
Step 2: After Polymorphism (15 min)
abstract class Shipping {
abstract calculate(weight: number): number
}
class StandardShipping extends Shipping {
calculate(weight: number) { return weight * 0.5 }
}
class ExpressShipping extends Shipping {
calculate(weight: number) { return weight * 1.5 + 10 }
}
Caller:
shipping.calculate(weight)
No switch. New type: add a class.
Step 3: When to Apply (15 min)
Switch / if-else on type
Multiple switches with same cases
Behavior varies by type
Adding new type touches many files
Step 4: When NOT (15 min)
One-shot switch
Simple value mapping (use a Map / record)
Types are stable forever
Hierarchy would be unwieldy for two cases
Step 5: The Steps (15 min)
Create abstract base class
Create subclasses
Move logic into subclasses
Replace caller code to use polymorphism
Run tests
Commit
Delete switch
Iterative.
Step 6: Strategy Pattern (15 min)
Polymorphism via composition:
interface ShippingStrategy {
calculate(weight: number): number
}
class Order {
constructor(private shipping: ShippingStrategy) {}
cost(weight: number) { return this.shipping.calculate(weight) }
}
Alternative to inheritance.
Step 7: Factory (15 min)
Where switch becomes a small factory:
class ShippingFactory {
static create(type: string): Shipping {
switch (type) {
case 'standard': return new StandardShipping()
...
}
}
}
One switch. Localized. Acceptable.
Step 8: Open / Closed (15 min)
Polymorphism enables OCP:
Open for extension (add new subclass)
Closed for modification (don't change existing)
For: code that changes often.
Step 9: Don't Overdo (15 min)
Two cases? Probably an if.
Six cases that share little? Polymorphism shines.
Right tool for the case.
Step 10: Maintain (15 min)
Adding new types should be:
Add a class
Add to factory (if used)
Done
No grep for switches; no risk of missing one.
That's the win.
What You Just Did
Replace Conditional with Polymorphism: the smell, after, when to apply, when not, the steps, strategy pattern, factory, open / closed, don't overdo, maintain.
Common Failure Modes
Polymorphism for 2 cases. Overkill.
Inheritance when composition fits. Tight coupling.
Forget factory. Switch survives elsewhere.
Behavior duplicated in subclasses. Missed common pattern.
No tests. Hierarchy refactor risky.
Continue the Refactoring Patterns path
Previous — Part 3: Extract Variable
Part of the Refactoring Patterns learning path.


