Applying Composition Over Inheritance
Quick start
- Identify the variation axis: data variant, algorithm, cross-cutting behavior, or framework constraint.
- Default to composition unless inheritance is explicitly justified by substitutability and stability.
- If the variant set is closed and data-shaped, prefer an ADT/sealed type over open inheritance.
- Produce a concrete migration plan with tests, rollout order, and review checkpoints.
Reference routing (progressive disclosure)
Read only the file needed for the current task:
- Foundations and mechanism choice → references/01-foundations-and-decision-making.md
- ADTs, composition patterns, frontend composition → references/02-adts-patterns-and-frontend-composition.md
- Step-by-step migration and code review checks → references/03-refactoring-and-review-checklist.md
- CI metrics, custom static checks, team adoption → references/04-metrics-and-adoption.md
Default workflow
-
Assess the current model
- List base classes, subclass count, hierarchy depth, and known pain points.
- Mark each variation as one of: data variant, behavior strategy, or infrastructure concern.
-
Choose mechanism (default-first)
- Choose composition for behavior variation, runtime swapping, or multi-axis change.
- Choose ADT/sealed type for closed, data-carrying variants requiring exhaustive handling.
- Choose inheritance only when all conditions hold: true is-a relation, Liskov-safe substitutability, and stable hierarchy.
-
Design the target shape
- Define minimal collaborator contracts; introduce interfaces/protocols where volatility, multiple implementations, or independent testing justify them.
- Use constructor injection for required collaborators.
- Keep inheritance depth shallow (target ≤2 below unavoidable framework bases).
-
Plan migration incrementally
- Add characterization tests first.
- Introduce a seam (interface/facade) before replacing internals.
- Migrate one subclass path at a time.
- Remove dead hierarchy pieces only after parity checks pass.
-
Validate and guardrail
- Run checklist-driven review.
- Add CI metrics/checks for DIT, override ratio, and decorator depth.
- Capture explicit waivers for framework-required inheritance.
Non-negotiable defaults
- Prefer composition by default.
- Do not introduce inheritance purely for code reuse.
- Keep collaborator interfaces narrow.
- Avoid service locator patterns; dependencies must be explicit.
- Treat deep decorator stacks (>3) and wide inheritance trees as design smells.
Output contract
When asked for guidance, return these sections in order:
- Decision: composition, ADT, or inheritance.
- Why: coupling, change axis, runtime needs, and testability implications.
- Design sketch: target interfaces/types and composition root.
- Migration steps: small reversible steps with validation points.
- Review/CI checks: checklist items and measurable thresholds.
Edge cases
- Framework-mandated inheritance: keep inheritance at the boundary; compose internally.
- Language lacks delegation sugar: use small helper generators/templates for forwarding.
- Performance-critical paths: measure dispatch overhead before adding indirection.
- Tiny stable domains: sealed inheritance may be acceptable when exhaustiveness is required.
Example
Input: “We have BaseReport with 12 subclasses and every new format adds another subclass. What should we do?”
Output (shape):
- Decision: Replace hierarchy with composition (Strategy + optional Decorator).
- Why: multi-axis variation and override-heavy subclasses indicate taxonomy drift.
- Design sketch:
ReportRenderer,HeaderPolicy,FooterPolicy,OutputFormatterinterfaces composed in oneReportService. - Migration: characterization tests → strategy extraction → per-subclass migration → subclass deletion.
- Guardrails: DIT warn >3, override ratio warn >0.5, decorator depth warn >3.