Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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

  1. Confirm the class uses the whole delegate interface and that nothing else holds a reference to the same delegate instance.
  2. 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.
  3. Initialize the superclass through super(...) with what the delegate was constructed with. Point the delegate field at this. Run the tests.
  4. Delete forwarding methods one at a time; the inherited versions take over. Run the tests after each.
  5. 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 BillingApiClient now publicly exposes generic delete(path); decide whether that is acceptable.
  • The inverse technique is Replace Inheritance with Delegation.

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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.