Parallel Inheritance Hierarchies
debt(d7/e7/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list phpmd and phpstan but explicitly marks automated as 'no', and the code_pattern description ('every new Animal subclass requiring a new AnimalRenderer subclass; class hierarchies growing in lockstep') requires a human reviewer to spot the cross-hierarchy coupling pattern. The smell is especially hard to catch when hierarchies live in different namespaces or packages, as noted in common_mistakes.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says hierarchies should be merged or one should reference the other — either path (merging two inheritance trees or introducing delegation throughout) touches multiple files and classes across the codebase. This is not a single-line patch nor a single-component change; it restructures the object model wherever both hierarchies are used.
Closest to 'strong gravitational pull' (b7). The smell applies across web, cli, and queue-worker contexts. Every new feature that touches either hierarchy must touch both, and the coupling is invisible (misconception: 'keeping them in sync is exactly the problem'). The parallel structure shapes how every new subclass is added, imposing a persistent, cross-cutting design tax on all maintainers.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field is explicit: developers believe parallel hierarchies are acceptable as long as they stay in sync, not realising that keeping them in sync IS the problem. Common_mistakes reinforce this — teams add classes to both hierarchies simultaneously 'because that's how it's always worked', never questioning the structural coupling until it becomes unmanageable.
Also Known As
TL;DR
Explanation
Parallel Inheritance Hierarchies occur when two class hierarchies mirror each other — adding a new Animal subclass also requires a new AnimalHandler subclass, and adding a Shape requires adding a ShapeRenderer. The hierarchies are tacitly coupled. The refactoring moves method implementations to one hierarchy using the Strategy or Visitor pattern, collapsing the parallel structure. This smell often appears when rendering, serialisation, or persistence concerns are baked into domain objects rather than separated via interfaces.
Common Misconception
Why It Matters
Common Mistakes
- Not noticing the smell because the two hierarchies are in different namespaces or packages.
- Adding classes to both hierarchies at the same time 'because that's how it's always worked'.
- Not considering delegation as a way to collapse parallel hierarchies into one.
- Parallel hierarchies that share naming prefixes — a reliable indicator: UserReport, AdminReport, GuestReport alongside UserFormatter, AdminFormatter, GuestFormatter.
Code Examples
// Every new shape requires a class in TWO hierarchies
class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
class ShapeDrawer {}
class CircleDrawer extends ShapeDrawer {}
class SquareDrawer extends ShapeDrawer {}
// Collapse via Strategy / Visitor — one hierarchy
interface Drawable {
public function draw(Canvas $canvas): void;
}
class Circle implements Drawable {
public function draw(Canvas $canvas): void { $canvas->drawCircle($this); }
}
class Square implements Drawable {
public function draw(Canvas $canvas): void { $canvas->drawSquare($this); }
}
// Adding a new shape = one class, not two