Refactoring techniqueDealing with Generalization
Collapse Hierarchy
Merge a subclass into its superclass (or the reverse) when the two have become nearly identical and the split no longer earns its cost.
Problem
A subclass adds almost nothing to its superclass. Perhaps features were moved away over time, or the subclass was created for a variation that never arrived. Readers have to open two files to understand one concept.
Solution
Move any remaining members into one of the two classes, redirect all references to it, and delete the other.
Before
class Notifier {
constructor(protected transport: Transport) {}
async notify(userId: string, message: string): Promise<void> {
const user = await users.find(userId);
if (user.mutedUntil && user.mutedUntil > new Date()) return;
await this.transport.send(user.contact, message);
}
}
// Created for a planned "priority" feature that never shipped
class DefaultNotifier extends Notifier {
constructor(transport: Transport) {
super(transport);
}
}
const notifier: Notifier = new DefaultNotifier(smtpTransport);After
class Notifier {
constructor(private transport: Transport) {}
async notify(userId: string, message: string): Promise<void> {
const user = await users.find(userId);
if (user.mutedUntil && user.mutedUntil > new Date()) return;
await this.transport.send(user.contact, message);
}
}
const notifier = new Notifier(smtpTransport);Why
- One class to read, with no question about which one to extend or instantiate.
protectedmembers can becomeprivateonce no subclass exists.
Steps
- Choose which class to keep. Usually keep the one whose name fits the concept and has more references.
- Move the remaining members of the removed class into the kept one with Pull Up Field, Pull Up Method, or their push-down counterparts. Run the tests.
- Replace every reference to the removed class (constructors, type annotations,
instanceof, DI bindings) with the kept class. Run the tests. - Delete the empty class. Tighten visibility of members that were
protectedonly for the subclass. Run the tests.
Watch out
- Do not collapse a hierarchy that other subclasses still extend, or that is part of a public extension point for library users.
- Check
instanceofchecks and serialized type discriminators ("type": "DefaultNotifier"in stored JSON or queues). Old data may still reference the removed class. - If the subclass carries real behavior for some instances, collapsing reintroduces flags and branches. That is the case for Extract Subclass.
- The inverse techniques are Extract Subclass and Extract Superclass.
Related
- Inverse: Extract Subclass, Extract Superclass.
- Fixes Lazy Class, Speculative Generality, and helps untangle Parallel Inheritance Hierarchies.
- The inheritance analog of Inline Class.
Agent checklist
- The removed class adds no meaningful behavior or state.
- No other code, plugin or public API extends the removed class.
- All references, including
instanceof, DI bindings and stored type names, were redirected. - Visibility was tightened after the collapse.
Treats these smells
- Lazy ClassA class does too little to justify the file, name and indirection it costs; fold it into its caller or delete it.
- Speculative GeneralityAbstractions, hooks and parameters built for future needs that never arrived make code harder to read without serving any current use.
- Parallel Inheritance HierarchiesEvery new subclass in one hierarchy forces a matching subclass in another, so two trees must always grow in lockstep.
See also
- Extract SubclassMove features used only by some instances of a class into a new subclass, replacing the flag or nullable fields that marked them.
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
- Inline ClassFold a class that does too little into the class that uses it, moving its fields and methods across and deleting it.
- Pull Up MethodMove a method that is identical across subclasses into their superclass so the logic lives and changes in one place.
- Push Down MethodMove a superclass method into the only subclasses that use it, so the superclass describes only what all subclasses share.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.