Skip to content
aipatterns.dev

Refactoring techniqueDealing with Generalization

Extract Subclass

Move features used only by some instances of a class into a new subclass, replacing the flag or nullable fields that marked them.

Problem

A class has fields and methods that only apply to some of its instances. A flag or a nullable field marks those instances, and methods check that flag before doing the special work.

Solution

Create a subclass for the special instances, and move the fields and methods that only they use into it. Construct the subclass wherever the flag used to be set.

Before

TypeScript
class Employee {
  constructor(
    readonly name: string,
    readonly baseSalaryCents: number,
    readonly isContractor: boolean,
    readonly hourlyRateCents: number | null,
  ) {}

  monthlyCost(hoursWorked: number): number {
    if (this.isContractor) return (this.hourlyRateCents ?? 0) * hoursWorked;
    return this.baseSalaryCents / 12;
  }

  holidayAllowanceDays(): number {
    return this.isContractor ? 0 : 25;
  }
}

After

TypeScript
class Employee {
  constructor(readonly name: string, readonly baseSalaryCents: number) {}

  monthlyCost(_hoursWorked: number): number {
    return this.baseSalaryCents / 12;
  }

  holidayAllowanceDays(): number {
    return 25;
  }
}

class Contractor extends Employee {
  constructor(name: string, readonly hourlyRateCents: number) {
    super(name, 0);
  }

  monthlyCost(hoursWorked: number): number {
    return this.hourlyRateCents * hoursWorked;
  }

  holidayAllowanceDays(): number {
    return 0;
  }
}

Why

  • The flag and its checks disappear; each class has straight-line logic.
  • hourlyRateCents is no longer nullable, because it exists only where it is set.
  • New contractor-only behavior has an obvious home.

Steps

  1. Create the empty subclass. Run the tests.
  2. Add a constructor or factory that creates the subclass, and change the places that set the flag to use it. Keep the flag working for now. Run the tests.
  3. Move fields used only by the special case into the subclass with Push Down Field. Run the tests.
  4. For each method that branches on the flag, override it in the subclass with the special branch, and remove that branch from the superclass. Run the tests after each method.
  5. Delete the flag when no code reads it.

Watch out

  • Do not use a subclass if an instance can switch categories during its life (an employee becoming a contractor). Objects cannot change class. Use Replace Type Code with State/Strategy or composition.
  • Do not use it if the variation is along more than one axis (contractor and remote and part-time). Subclasses multiply. Extract a component with Extract Class instead.
  • In the example, Contractor still inherits baseSalaryCents. If many such leftovers appear, the hierarchy is wrong; a shared superclass of two siblings may fit better.
  • The inverse is Collapse Hierarchy.

Agent checklist

  • The special case is fixed at construction and never changes.
  • Variation is along a single axis.
  • Each flag branch moved to an override with tests green in between.
  • The flag and its now-dead branches were deleted.

Treats these smells

See also

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