Refactoring techniqueDealing with Generalization
Replace Delegation with Inheritance
When a class forwards nearly every call to one delegate it truly is a kind of, make it a subclass of that delegate and delete the forwarding.
Problem
A class holds an instance of another class and forwards almost every method to it unchanged. Each new method on the delegate needs another forwarding method. The class is really a specialization of the delegate, but it is written as a wrapper.
Solution
Make the class extend the delegate's class, remove the delegate field, and delete the forwarding methods. Keep only the methods that add or change behavior.
Before
class HttpClient {
constructor(protected baseUrl: string) {}
get<T>(path: string): Promise<T> { /* ... */ }
post<T>(path: string, body: unknown): Promise<T> { /* ... */ }
put<T>(path: string, body: unknown): Promise<T> { /* ... */ }
delete(path: string): Promise<void> { /* ... */ }
}
class BillingApiClient {
private http = new HttpClient("https://billing.internal/v2");
get<T>(path: string) { return this.http.get<T>(path); }
post<T>(path: string, body: unknown) { return this.http.post<T>(path, body); }
put<T>(path: string, body: unknown) { return this.http.put<T>(path, body); }
delete(path: string) { return this.http.delete(path); }
invoice(id: string) { return this.http.get<Invoice>(`/invoices/${id}`); }
}After
class HttpClient {
constructor(protected baseUrl: string) {}
get<T>(path: string): Promise<T> { /* ... */ }
post<T>(path: string, body: unknown): Promise<T> { /* ... */ }
put<T>(path: string, body: unknown): Promise<T> { /* ... */ }
delete(path: string): Promise<void> { /* ... */ }
}
class BillingApiClient extends HttpClient {
constructor() {
super("https://billing.internal/v2");
}
invoice(id: string) { return this.get<Invoice>(`/invoices/${id}`); }
}Why
- The forwarding boilerplate disappears, along with the work of keeping it in sync.
- New methods on the base class are available automatically.
- The class shrinks to what makes it different.
Steps
- Confirm the class uses the whole delegate interface and that nothing else holds a reference to the same delegate instance.
- Make the class extend the delegate's class. If a forwarding method has the same name, it now overrides the inherited one and still works. Run the tests.
- Initialize the superclass through
super(...)with what the delegate was constructed with. Point the delegate field atthis. Run the tests. - Delete forwarding methods one at a time; the inherited versions take over. Run the tests after each.
- Replace remaining uses of the field with
this, then delete the field. Run the tests.
Watch out
- Do not do this if the class uses only part of the delegate. Inheritance exposes the whole interface, which is the Refused Bequest problem the inverse technique exists to fix.
- Do not do this when the delegate is shared with other objects or swapped at runtime. A superclass cannot be shared or replaced.
- If the class already extends something else, single-inheritance languages block this. Consider Remove Middle Man so clients call the delegate directly.
- Inheritance couples you to the base class's protected internals and future changes. In the example, a
BillingApiClientnow publicly exposes genericdelete(path); decide whether that is acceptable. - The inverse technique is Replace Inheritance with Delegation.
Related
- Inverse: Replace Inheritance with Delegation.
- Fixes Middle Man.
- Alternative: Remove Middle Man.
Agent checklist
- The class forwards nearly all of the delegate's public methods unchanged.
- The delegate is not shared with other objects or replaced at runtime.
- The class does not already need a different superclass.
- Exposing the full inherited interface is acceptable for callers.
Treats these smells
See also
- Replace Inheritance with DelegationTurn a subclass that uses only part of its superclass into a class that holds an instance of it and forwards just the calls it needs.
- Remove Middle ManLet clients call the delegate directly when a class does little more than forward calls to it.
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.