Replace Subclass with Delegation
Intent
Section titled “Intent”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.
The smell
Section titled “The smell”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.
Before → After
Section titled “Before → After”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 tierabstract 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 strategyinterface 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); }}# Before — one subclass per pricing tierclass Booking: def __init__(self, base): self.base = base
def price(self): raise NotImplementedError
class RegularBooking(Booking): def price(self): return self.base
class PremiumBooking(Booking): def price(self): return self.base * 1.5 + 20
# After — one Booking that delegates pricing to a strategyclass RegularPricing: def price(self, base): return base
class PremiumPricing: def price(self, base): return base * 1.5 + 20
class Booking: def __init__(self, base, pricer): self.base = base self.pricer = pricer
def price(self): return self.pricer.price(self.base)// Go has no subclasses to begin with. The idiomatic shape is exactly the// "after": a Booking value holds a PricingPolicy interface and delegates.
type PricingPolicy interface { Price(base float64) float64}
type RegularPricing struct{}
func (RegularPricing) Price(base float64) float64 { return base }
type PremiumPricing struct{}
func (PremiumPricing) Price(base float64) float64 { return base*1.5 + 20 }
type Booking struct { Base float64 Pricer PricingPolicy}
func (b Booking) Price() float64 { return b.Pricer.Price(b.Base)}
// booking := Booking{Base: 100, Pricer: PremiumPricing{}}// Rust has no subclasses. The strategy is a trait object (or a generic// type parameter) held by the single Booking struct.
trait PricingPolicy { fn price(&self, base: f64) -> f64;}
struct RegularPricing;impl PricingPolicy for RegularPricing { fn price(&self, base: f64) -> f64 { base }}
struct PremiumPricing;impl PricingPolicy for PremiumPricing { fn price(&self, base: f64) -> f64 { base * 1.5 + 20.0 }}
struct Booking { base: f64, pricer: Box<dyn PricingPolicy>,}
impl Booking { fn price(&self) -> f64 { self.pricer.price(self.base) }}
// let booking = Booking { base: 100.0, pricer: Box::new(PremiumPricing) };classDiagram
class Booking {
-pricer: PricingPolicy
+price() number
}
class PricingPolicy {
<<interface>>
+price(base) number
}
class RegularPricing
class PremiumPricing
Booking --> PricingPolicy : delegates
PricingPolicy <|.. RegularPricing
PricingPolicy <|.. PremiumPricing Mechanics
Section titled “Mechanics”- Identify the single axis along which the subclasses vary — the method (or small cluster of methods) whose body differs.
- Define a strategy interface (interface in TypeScript/Go, protocol or duck-typed class in Python, trait in Rust) capturing that varying behaviour.
- For each existing subclass, create a strategy implementation that holds its version of the varying logic.
- Add a field on the base class to hold a strategy, and make the base class delegate the varying method to that field.
- Replace each
new SubclassX()call withnew Base(..., new StrategyX()). Run your tests. - Delete the now-empty subclasses.
When to use / trade-offs
Section titled “When to use / trade-offs”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.