Replace Conditional with Polymorphism
What This Concept Is
This move distributes type-specific branches into implementations selected through a shared operation. A hierarchy or supplied strategy can support an extension boundary; compare it with a closed match before choosing the move.
Fowler:
I remove the duplication of the common switch logic by creating classes for each case and using polymorphism to bring out the type-specific behavior.
Recurrence is one useful signal. A single extensible boundary may justify polymorphism, while repeated matches over a closed variant set may be appropriate. Compare the expected extension axis, lifecycle and cost.
Why It Matters Here
This is the move that most often creates the occasion for a pattern (Strategy, State, Template Method). It is also the move that is most often misapplied: learners see one switch and immediately build a three-class hierarchy, trading a 10-line conditional for 40 lines of ceremony.
Neither recurrence nor class count establishes a benefit. Adding variants and adding operations put different pressures on a hierarchy or a closed tagged representation.
Concrete Example
Before -- the same switch appears in plumage and in (say) airSpeedVelocity:
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return 'average';
case 'AfricanSwallow':
return bird.numberOfCoconuts > 2 ? 'tired' : 'average';
case 'NorwegianBlueParrot':
return bird.voltage > 100 ? 'scorched' : 'beautiful';
default:
return 'unknown';
}
}
// plus airSpeedVelocity with the same shape, elsewhere.
After -- one hierarchy, two polymorphic methods:
class Bird {
get plumage() { return 'unknown'; }
}
class EuropeanSwallow extends Bird {
get plumage() { return 'average'; }
}
class AfricanSwallow extends Bird {
constructor(data) { super(); this.numberOfCoconuts = data.numberOfCoconuts; }
get plumage() { return this.numberOfCoconuts > 2 ? 'tired' : 'average'; }
}
class NorwegianBlueParrot extends Bird {
constructor(data) { super(); this.voltage = data.voltage; }
get plumage() { return this.voltage > 100 ? 'scorched' : 'beautiful'; }
}
function birdFor(data) {
switch (data.type) {
case 'EuropeanSwallow': return new EuropeanSwallow(data);
case 'AfricanSwallow': return new AfricanSwallow(data);
case 'NorwegianBlueParrot':return new NorwegianBlueParrot(data);
default: return new Bird();
}
}
This factory uses a switch; a registry or caller-supplied strategy is another option. Only plumage is implemented here; airSpeedVelocity still needs its own branch migration and checks. Copying fields also changes behavior if callers expect later mutations of the original record to remain visible.
Common Confusion / Misconception
Polymorphism is an option for a single conditional when a useful extension boundary exists. It is not compulsory for repeated conditions. Decomposition, a function table and leaving a small match intact are alternatives.
If the discriminator changes during an object's lifetime, model the state transition explicitly. A State object may help; a simple state machine may be sufficient. Preserve default/unknown cases and mutable-input semantics during migration.
How To Use It
- Define the supported variants, default behavior and ownership of mutable data.
- Compare adding a variant with adding a new operation under each candidate design.
- Preserve the public entry point while migrating one branch at a time.
- Check every branch, boundary and unknown case, including retained aliases.
- Remove obsolete dispatch only when caller compatibility is established.
Check Yourself
- Why can a single conditional justify an extension boundary?
- What changes if birdFor copies mutable input fields?
- When would a closed match be cheaper than a hierarchy?
Mini Drill or Application
For the bird example, compare a retained input record with a snapshot taken at construction. Change voltage after construction and predict both observations. Specify the intended contract, then defend a hierarchy, a function table or the existing match under a stated extension axis.
Engineering Extension: Compare both directions of extension
With stable operations and externally added variants, dispatch through supplied behavior may help. With a closed set of variants and frequent new analyses, external operations may be simpler. Preserve unknown-case results and failures during branch migration; replacing unknown with a default valid variant is a behavior change.
Use the behavior contracts, labs and practice feedback to test the claim.
Read This Only If Stuck
Locate this move through the Refactoring book index. The selective reading guide gives exact consulted local locators and primary documentation. Index links are not exact chapter links.