Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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 Subscriber and treat both kinds the same.
  • The shared concept gets a name.

Steps

  1. Create an empty abstract superclass and make both classes extend it. Run the tests.
  2. Unify names of matching fields and methods in both classes (errorCount becomes failures). Run the tests.
  3. Move common fields up with Pull Up Field. Run the tests.
  4. Move identical methods up with Pull Up Method. Declare methods with the same signature but different bodies as abstract in the superclass. Run the tests.
  5. 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 FailureCounter object may age better than inheritance.
  • The inverse is Collapse Hierarchy.

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

See also

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