Skip to content

Replace Subclass with Delegation

Replace Subclass with Delegation collapses a family of subclasses that differ along one axis of variation into a single class that holds a strategy object. Instead of RegularBooking and PremiumBooking subclasses, you have one Booking that delegates its varying behaviour to an injected PricingPolicy. The variation moves from the type hierarchy into a field you can set — even change at runtime.

Subclassing is a static, one-time choice: an object is a PremiumBooking forever, and if the same booking varies along a second axis too — say weekday versus weekend pricing and refundable versus non-refundable — you face a combinatorial explosion of subclasses. When the difference between subclasses is really just “which algorithm runs here”, that difference belongs in a delegate, not in the type. This is the classic move from inheritance to the Strategy pattern.

A booking whose price depends on its tier. Before, each tier is a subclass. After, the tier is a PricingPolicy the single Booking delegates to.

// Before — one subclass per pricing tier
abstract class Booking {
constructor(protected base: number) {}
abstract price(): number;
}
class RegularBooking extends Booking {
price(): number { return this.base; }
}
class PremiumBooking extends Booking {
price(): number { return this.base * 1.5 + 20; }
}
// After — one Booking that delegates pricing to a strategy
interface PricingPolicy {
price(base: number): number;
}
class RegularPricing implements PricingPolicy {
price(base: number): number { return base; }
}
class PremiumPricing implements PricingPolicy {
price(base: number): number { return base * 1.5 + 20; }
}
class Booking {
constructor(private base: number, private pricer: PricingPolicy) {}
price(): number { return this.pricer.price(this.base); }
}
classDiagram
  class Booking {
    -pricer: PricingPolicy
    +price() number
  }
  class PricingPolicy {
    <<interface>>
    +price(base) number
  }
  class RegularPricing
  class PremiumPricing
  Booking --> PricingPolicy : delegates
  PricingPolicy <|.. RegularPricing
  PricingPolicy <|.. PremiumPricing
A tier hierarchy becomes one Booking delegating to a PricingPolicy
  1. Identify the single axis along which the subclasses vary — the method (or small cluster of methods) whose body differs.
  2. Define a strategy interface (interface in TypeScript/Go, protocol or duck-typed class in Python, trait in Rust) capturing that varying behaviour.
  3. For each existing subclass, create a strategy implementation that holds its version of the varying logic.
  4. Add a field on the base class to hold a strategy, and make the base class delegate the varying method to that field.
  5. Replace each new SubclassX() call with new Base(..., new StrategyX()). Run your tests.
  6. Delete the now-empty subclasses.

Reach for this when subclasses differ only in a swappable algorithm, when an object needs to change its variant at runtime (something inheritance cannot do), or when two independent axes of variation would otherwise multiply into a subclass explosion — composing two strategy fields scales linearly where subclasses scale multiplicatively.

The trade-off is one extra object and a layer of indirection: callers now wire up a strategy when they build the base object. That is usually worth it, and in Go and Rust it is simply the normal way to vary behaviour — neither language offers a subclass hierarchy to replace, so you write the delegating form from the outset.

Replace Subclass with Delegation is ideal when subclasses differ along how many axes of variation?
What design pattern does this refactoring move toward?
What can a strategy field do that a subclass choice cannot?
Why is this already the default shape in Go and Rust?