Skip to content

Replace Inheritance with Delegation

Replace Inheritance with Delegation unhooks a subclass from its parent and instead gives it a field holding an instance of the former parent. The class forwards (delegates) the calls it genuinely needs and ignores the rest. You do this when a subclass uses only a fraction of its superclass’s interface, or when the inheritance modelled a “has-a” relationship dressed up as “is-a”.

The classic symptom is Refused Bequest: Stack extends List, but a stack should not let callers insertAt the middle or remove from the bottom. By inheriting, Stack exposes the entire List interface and silently breaks its own invariants. The relationship is wrong — a stack has a list, it is not a list. Replacing inheritance with delegation lets Stack expose only push and pop, keeping the list private.

A Stack built by extending List leaks every list operation. After the refactoring it holds a list and exposes only stack operations.

// Before — Stack IS-A List, leaking add/get/removeAt to callers
class List<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
removeLast(): T | undefined { return this.items.pop(); }
size(): number { return this.items.length; }
}
class Stack<T> extends List<T> {
push(item: T): void { this.add(item); }
pop(): T | undefined { return this.removeLast(); }
}
// After — Stack HAS-A List and delegates only what it needs
class Stack<T> {
private list = new List<T>();
push(item: T): void { this.list.add(item); }
pop(): T | undefined { return this.list.removeLast(); }
size(): number { return this.list.size(); }
}
classDiagram
  class List {
    +add(item)
    +size() number
  }
  class Stack
  Stack --> List : holds & delegates
  note for Stack "Stack HAS-A List, not IS-A List"
Stack stops being a List and starts holding one
  1. Create a field in the subclass to hold an instance of the (former) superclass. Initialise it — either with a fresh instance or with the object itself if you are unwrapping a self-reference.
  2. For each superclass method the subclass actually uses, add a delegating method that forwards the call to the field.
  3. Remove the extends / embedding relationship so the class no longer inherits the parent’s full interface.
  4. Adjust callers that relied on inherited members they should never have reached — this is the leak you are sealing.
  5. Run your tests after each delegating method is wired up.
  6. Once delegation is in place, you are free to narrow the exposed interface and protect invariants the old inheritance violated.

Use this when a subclass refuses much of its bequest, when inheritance is leaking operations that break the subclass’s own rules, or when “is-a” was never really true. After the move you control exactly which operations are public, so invariants become enforceable.

The cost is a little forwarding boilerplate — one short method per delegated operation. That is a small, honest price for an accurate relationship. In Go this is simply choosing a held field over embedding; in Rust delegation is the only path, so this refactoring is just “write idiomatic Rust from the start”.

What problem does Replace Inheritance with Delegation solve?
After the refactoring, how does the class reuse the former parent?
Why is this refactoring "free" in Rust?
What is the main cost of choosing delegation over inheritance?