Skip to main content

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​

  1. Define the supported variants, default behavior and ownership of mutable data.
  2. Compare adding a variant with adding a new operation under each candidate design.
  3. Preserve the public entry point while migrating one branch at a time.
  4. Check every branch, boundary and unknown case, including retained aliases.
  5. Remove obsolete dispatch only when caller compatibility is established.

Check Yourself​

  1. Why can a single conditional justify an extension boundary?
  2. What changes if birdFor copies mutable input fields?
  3. 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.