Skip to content
aipatterns.dev

Refactoring techniqueDealing with Generalization

Collapse Hierarchy

Merge a subclass into its superclass (or the reverse) when the two have become nearly identical and the split no longer earns its cost.

Problem

A subclass adds almost nothing to its superclass. Perhaps features were moved away over time, or the subclass was created for a variation that never arrived. Readers have to open two files to understand one concept.

Solution

Move any remaining members into one of the two classes, redirect all references to it, and delete the other.

Before

TypeScript
class Notifier {
  constructor(protected transport: Transport) {}

  async notify(userId: string, message: string): Promise<void> {
    const user = await users.find(userId);
    if (user.mutedUntil && user.mutedUntil > new Date()) return;
    await this.transport.send(user.contact, message);
  }
}

// Created for a planned "priority" feature that never shipped
class DefaultNotifier extends Notifier {
  constructor(transport: Transport) {
    super(transport);
  }
}

const notifier: Notifier = new DefaultNotifier(smtpTransport);

After

TypeScript
class Notifier {
  constructor(private transport: Transport) {}

  async notify(userId: string, message: string): Promise<void> {
    const user = await users.find(userId);
    if (user.mutedUntil && user.mutedUntil > new Date()) return;
    await this.transport.send(user.contact, message);
  }
}

const notifier = new Notifier(smtpTransport);

Why

  • One class to read, with no question about which one to extend or instantiate.
  • protected members can become private once no subclass exists.

Steps

  1. Choose which class to keep. Usually keep the one whose name fits the concept and has more references.
  2. Move the remaining members of the removed class into the kept one with Pull Up Field, Pull Up Method, or their push-down counterparts. Run the tests.
  3. Replace every reference to the removed class (constructors, type annotations, instanceof, DI bindings) with the kept class. Run the tests.
  4. Delete the empty class. Tighten visibility of members that were protected only for the subclass. Run the tests.

Watch out

  • Do not collapse a hierarchy that other subclasses still extend, or that is part of a public extension point for library users.
  • Check instanceof checks and serialized type discriminators ("type": "DefaultNotifier" in stored JSON or queues). Old data may still reference the removed class.
  • If the subclass carries real behavior for some instances, collapsing reintroduces flags and branches. That is the case for Extract Subclass.
  • The inverse techniques are Extract Subclass and Extract Superclass.

Agent checklist

  • The removed class adds no meaningful behavior or state.
  • No other code, plugin or public API extends the removed class.
  • All references, including instanceof, DI bindings and stored type names, were redirected.
  • Visibility was tightened after the collapse.

Treats these smells

See also

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