top of page

Replace Conditional with Polymorphism — Refactoring Patterns, Part 4

Shawn West
Jul 28
2 min read

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)

  1. Create abstract base class

  2. Create subclasses

  3. Move logic into subclasses

  4. Replace caller code to use polymorphism

  5. Run tests

  6. Commit

  7. 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

Part of the Refactoring Patterns learning path.

bottom of page