Skip to content

Replace Superclass with Delegation

Replace Superclass with Delegation turns an inheritance link into a has-a link. Instead of Stack extends List, the stack holds a list in a private field and calls into it for the handful of operations it actually needs. The subclass keeps the behaviour it relied on but is no longer forced to inherit — and expose — every member of a parent it never wanted in full.

A class extends another to reuse a few methods and ends up dragging the whole public interface along with it. Stack extends List inherits add, remove, get(index), clear — so callers can now stack.remove(item) from the middle, violating the very invariant a stack exists to protect. The “is-a” claim is false: a stack is not a kind of list, it merely uses one. The inherited interface is awkward, leaky, or oversized — a textbook case of Refused Bequest, where the heir quietly disowns much of what it was left.

A Stack that extends List to get its storage, leaking list operations. After, it holds a List and exposes only push/pop/size.

// Before — Stack inherits the entire List interface
class List<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
remove(item: T): void {
const i = this.items.indexOf(item);
if (i >= 0) this.items.splice(i, 1);
}
get(index: number): T { return this.items[index]; }
size(): number { return this.items.length; }
}
// A Stack should not expose remove(item) or get(index)!
class Stack<T> extends List<T> {
push(item: T): void { this.add(item); }
pop(): T | undefined { return undefined; /* awkward */ }
}
// After — Stack holds a List and delegates only what it needs
class Stack<T> {
private storage = new List<T>();
push(item: T): void { this.storage.add(item); }
pop(): T | undefined {
const n = this.storage.size();
if (n === 0) return undefined;
const top = this.storage.get(n - 1);
this.storage.remove(top);
return top;
}
size(): number { return this.storage.size(); }
}
classDiagram
  class List~T~ {
    +add(item)
    +remove(item)
    +size() number
    +clear()
  }
  class Stack~T~
  List <|-- Stack : before (extends)
  class StackAfter["Stack"] {
    -items: List~T~
    +push(item)
    +pop() T
  }
  StackAfter --> List : after (holds & delegates)
Stack stops extending List and instead holds one, delegating only what it needs
  1. Add a field to the subclass that holds an instance of the former superclass; initialise it (often by constructing a fresh one, or by accepting it as a constructor argument).
  2. For each inherited member the subclass genuinely uses, add a small forwarding method that delegates to the field.
  3. Update the subclass’s own methods to call the field instead of super.
  4. Remove the extends / inheritance link so the subclass no longer subtypes the parent. Run your tests after each member you migrate.
  5. Trim the delegating surface to only the operations the class should actually expose — the awkward inherited members simply vanish from its interface.

Reach for this when a subclass uses only a fragment of its parent, when inheriting the parent leaks operations that break the subclass’s invariants, or when the “is-a” relationship is really “uses-a”. Delegation lets you expose exactly the right interface and keeps the two classes free to evolve independently.

The cost is the forwarding methods you write by hand for the members you do keep — pure inheritance would have given those for free. When a subclass legitimately is its parent and wants the full interface, leave the inheritance alone. The inverse is Replace Delegation with Inheritance, worth doing only when you find yourself forwarding the entire interface verbatim. Go and Rust make the choice for you: with no inheritance, you hold a field and delegate from the start — and Go’s struct embedding is a deliberate halfway tool you reach for only when you want the whole interface promoted.

What situation calls for Replace Superclass with Delegation?
After the refactoring, how does the former subclass reach the behaviour it needs?
Which code smell does inheriting an oversized interface exemplify?
Why does Go use a NAMED field rather than struct embedding for this case?