Skip to content
aipatterns.dev

Code smellDispensables

Lazy Class

A class does too little to justify the file, name and indirection it costs; fold it into its caller or delete it.

Also known as
Freeloader

Signs

  • A class has one short method, or only forwards calls to another object.
  • A subclass adds nothing over its parent, or overrides one trivial value.
  • A class was planned for features that never arrived.
  • After a refactoring, a class is left holding one field and a getter.
  • Readers have to open the file to learn that it does almost nothing.

Why it happens

Classes shrink over time as responsibilities move elsewhere, and nobody deletes the shell. Others are created up front for a design that was never built. Some come from a rule like "every concept gets its own class", applied to concepts that are a single value or a single line.

Example

A billing module wraps a currency formatter in its own class and a subclass.

billing.ts
class MoneyFormatter {
  format(cents: number): string {
    return (cents / 100).toFixed(2);
  }
}

class UsdMoneyFormatter extends MoneyFormatter {
  format(cents: number): string {
    return "$" + super.format(cents);
  }
}

class InvoiceView {
  private formatter = new UsdMoneyFormatter();
  totalLine(totalCents: number): string {
    return `Total: ${this.formatter.format(totalCents)}`;
  }
}

Two classes and an inheritance link wrap one expression. Inline them:

billing.ts
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

class InvoiceView {
  totalLine(totalCents: number): string {
    return `Total: ${usd.format(totalCents / 100)}`;
  }
}

The platform formatter also handles thousands separators and negative values, which the hand-written class did not.

Treatment

  • If a class does little and has one main user, use Inline Class to move its members into that user.
  • If a subclass or superclass adds almost nothing, use Collapse Hierarchy to merge it into its neighbor.
  • If the class only forwards calls, treat it as Middle Man and use Remove Middle Man.
  • If a standard library or platform feature already does the job, delete the class and call the platform directly.

Payoff

  • Fewer files and fewer jumps to follow the code.
  • The logic sits next to its only caller, where readers expect it.
  • Less surface to keep in sync during later refactorings.

When to ignore

  • The class marks a real boundary: an interface you mock in tests, a seam for a third-party dependency, or a public API type.
  • It is a value type that gives a primitive a name and validation (for example EmailAddress). That cures Primitive Obsession, even if it is small.
  • The class is a deliberate placeholder for work scheduled in the current iteration, not "someday".
  • Framework conventions require it (a controller, a migration, a DI module).

Agent checklist

  • You listed every user of the class and confirmed none needs it as a seam or mock point.
  • The class is not a value object that protects an invariant.
  • After inlining, no caller lost behavior, including edge cases the old class handled.
  • Imports, DI registrations and exports referencing the class were removed.
  • Tests pass and test doubles for the removed class were deleted or replaced.

Treatments

See also

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