Skip to content

Collapse Hierarchy

Collapse Hierarchy merges a superclass and a subclass that are no longer different enough to justify being two separate types. The members of one are folded into the other, the now-redundant class is deleted, and every reader is spared a layer of indirection that bought them nothing.

Hierarchies erode. Refactorings pull members up and push them down until a subclass adds almost nothing — maybe one trivial field, maybe an override that now matches the parent. This is Lazy Class wearing a subclass costume: an extra type, an extra file, an extra hop for every reader, all to express a distinction that has quietly disappeared. When a subclass and its superclass have become near-twins, the honest move is to make them one.

A Salesperson subclass once meaningfully extended Employee, but successive refactorings left it holding a single field that Employee could just as well own. We collapse them.

// Before — Salesperson adds almost nothing over Employee
class Employee {
constructor(public name: string, public monthlyPay: number) {}
annualCost(): number { return this.monthlyPay * 12; }
}
class Salesperson extends Employee {
constructor(name: string, monthlyPay: number, public region: string) {
super(name, monthlyPay);
}
}
// After — one Employee carries the region field directly
class Employee {
constructor(
public name: string,
public monthlyPay: number,
public region: string = "",
) {}
annualCost(): number { return this.monthlyPay * 12; }
}
classDiagram
  class Employee {
    +name string
    +grade number
    +annualCost() number
  }
  note for Employee "Salesperson merged back in — one type now"
Two near-identical types become one Employee
  1. Decide which class survives. Usually the superclass absorbs the subclass, but if the subclass name reads better for the merged concept, keep it instead.
  2. Use Pull Up and Push Down to move all fields and methods into the single surviving class, so the doomed class is left empty.
  3. Repoint every reference and constructor call from the deleted class to the survivor. Run your tests.
  4. Delete the now-empty class (and remove the embedding/wrapper in Go and Rust).
  5. Run your tests one final time. The program should behave exactly as before, with one fewer type to navigate.

Collapse a hierarchy when a subclass adds so little that the extra type is pure overhead — no meaningful behaviour, no real distinction, just indirection. Removing it makes the code shorter and flatter, and readers stop hopping between parent and child to assemble one concept.

The trade-off is reversibility: if the distinction returns later, you will re-extract a subclass (or interface). That is fine — Collapse Hierarchy is the natural inverse of Extract Superclass, and refactoring is meant to flow both ways as your understanding changes. In Go and Rust there was never a class hierarchy to begin with, so “collapse” simply means deleting a needless wrapper struct and inlining its one field.

When is Collapse Hierarchy the right move?
Collapse Hierarchy is the natural inverse of which refactoring?
Which smell does an almost-empty subclass exhibit?
What does "collapse" mean in Go or Rust, which have no inheritance?