Refactoring techniqueDealing with Generalization
Extract Superclass
Create a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
Problem
Two unrelated classes have the same fields and some of the same methods. They were built separately, so the shared logic is duplicated and may already have drifted apart.
Solution
Create a new superclass, make both classes extend it, and move the shared fields and methods up.
Before
class WebhookEndpoint {
constructor(readonly url: string, readonly secret: string) {}
private failures = 0;
recordFailure(): void { this.failures++; }
isDisabled(): boolean { return this.failures >= 5; }
deliver(event: DomainEvent) { return http.post(this.url, sign(event, this.secret)); }
}
class SlackIntegration {
constructor(readonly webhookUrl: string, readonly channel: string) {}
private errorCount = 0;
recordFailure(): void { this.errorCount++; }
isDisabled(): boolean { return this.errorCount >= 5; }
deliver(event: DomainEvent) { return http.post(this.webhookUrl, { channel: this.channel, text: summarize(event) }); }
}After
abstract class Subscriber {
private failures = 0;
recordFailure(): void { this.failures++; }
isDisabled(): boolean { return this.failures >= 5; }
abstract deliver(event: DomainEvent): Promise<unknown>;
}
class WebhookEndpoint extends Subscriber {
constructor(readonly url: string, readonly secret: string) { super(); }
deliver(event: DomainEvent) { return http.post(this.url, sign(event, this.secret)); }
}
class SlackIntegration extends Subscriber {
constructor(readonly webhookUrl: string, readonly channel: string) { super(); }
deliver(event: DomainEvent) { return http.post(this.webhookUrl, { channel: this.channel, text: summarize(event) }); }
}Why
- Failure tracking exists once, so the disable threshold cannot differ by accident.
- Code that dispatches events can work with
Subscriberand treat both kinds the same. - The shared concept gets a name.
Steps
- Create an empty abstract superclass and make both classes extend it. Run the tests.
- Unify names of matching fields and methods in both classes (
errorCountbecomesfailures). Run the tests. - Move common fields up with Pull Up Field. Run the tests.
- Move identical methods up with Pull Up Method. Declare methods with the same signature but different bodies as
abstractin the superclass. Run the tests. - Change clients that can now depend on the superclass instead of either concrete class.
Watch out
- Do not extract a superclass when either class already extends something else in a single-inheritance language. Use Extract Class and composition, or Extract Interface for the shared contract.
- Do not create a superclass for classes that share code by coincidence but not a concept. A shared helper function is lighter and couples less.
- Shared state in a base class couples subclasses tightly. If the shared part is a self-contained concern (like failure tracking), a composed
FailureCounterobject may age better than inheritance. - The inverse is Collapse Hierarchy.
Related
- Inverse: Collapse Hierarchy.
- Fixes Duplicate Code and Alternative Classes with Different Interfaces.
- Alternatives: Extract Class (composition), Extract Interface (contract only).
Agent checklist
- Both classes represent the same concept, not only similar code.
- Neither class already needs a different base class.
- Names were unified before members were pulled up.
- Composition was considered for self-contained shared concerns.
Treats these smells
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
- Alternative Classes with Different InterfacesTwo classes do the same job but expose different method names or signatures, so callers cannot swap one for the other.
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 InterfaceDeclare the subset of a class's methods that clients actually use as an interface, so clients depend on the contract instead of the class.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Pull Up FieldMove a field that every subclass declares in the same way into their shared superclass so it is defined once.
- Pull Up MethodMove a method that is identical across subclasses into their superclass so the logic lives and changes in one place.
- Collapse HierarchyMerge a subclass into its superclass (or the reverse) when the two have become nearly identical and the split no longer earns its cost.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.