Generalization & Inheritance
Intent
Section titled “Intent”The generalization and inheritance family moves features vertically and sideways across a hierarchy of related types. A member that two siblings share is pulled up to their parent. A member only one branch needs is pushed down. Two unrelated classes that have grown alike grow a common parent. And when inheritance turns out to be the wrong tool — when a subclass uses only a sliver of its parent, or the “is-a” relationship was a lie — these moves let you swap it for composition without changing what the program does.
The smell
Section titled “The smell”Inheritance is powerful and easy to overreach with. Identical methods get copy-pasted into three subclasses. A field lives in the superclass but only half the subclasses touch it. A class extends another just to borrow two helpers and inherits a dozen it must now pretend not to have. Each of these is a hierarchy that no longer tells the truth about how the types relate. The refactorings here straighten the hierarchy out — or dissolve it into delegation when composition fits better.
classDiagram
class Superclass {
+shared() members
}
class SubclassA
class SubclassB
Superclass <|-- SubclassA : pull up / push down
Superclass <|-- SubclassB : extract superclass
class Interface {
<<interface>>
+protocol()
}
Interface <|.. SubclassA : extract interface
class Delegate {
+behaviour()
}
SubclassB --> Delegate : replace inheritance / subclass with delegation What this module covers
Section titled “What this module covers”Eight refactorings, each reversible:
- Pull Up Method / Field — when siblings share an identical member, move it to the superclass so it lives in exactly one place. Push Down is the inverse: a member only some subclasses need drops into those subclasses.
- Extract Superclass / Interface — when two classes share features, lift the commonality into a new parent; when they share only a protocol, extract an interface (a trait in Rust).
- Replace Inheritance with Delegation — when a subclass leans on only part of its parent, hold the former parent as a field and delegate to it.
- Replace Subclass with Delegation — when subclasses vary along a single axis, replace the whole hierarchy with one class holding a strategy object.
- Collapse Hierarchy — when a subclass and its superclass have drifted too close to justify two types, merge them into one.
- Replace Subclass with Fields — when subclasses differ only by constant data returned from their methods, collapse them into one class whose fields hold that data.
- Replace Superclass with Delegation — when a subclass uses only part of its superclass or inherits an awkward interface, hold the former superclass as a field and delegate to it.
- Pull Up Constructor Body — when sibling constructors share the same initialization, move that common construction into the superclass constructor and call up to it.
Each lesson shows the same before-and-after in TypeScript, Python, Go, and Rust. Note that Go and Rust have no class inheritance: Go composes behaviour through struct embedding and interfaces, and Rust through traits and held fields. So the examples in those languages model the same intent with composition — which, as you will see, is exactly where several of these refactorings are trying to lead you anyway.