Skip to content
aipatterns.dev

Code smellDispensables

Speculative Generality

Abstractions, hooks and parameters built for future needs that never arrived make code harder to read without serving any current use.

Also known as
YAGNI violation, Over-engineering

Signs

  • An interface has exactly one implementation, and no test double uses it either.
  • An abstract base class has one subclass.
  • A function takes options or parameters that every caller passes with the same value, or never passes.
  • A plugin system, registry or strategy hook exists with one plugin.
  • Names are generic (DataProcessor, handleEntity, GenericRepository<T>) for code that handles one concrete thing.
  • The only callers of some methods are their own tests.

Why it happens

Developers anticipate change and build the flexibility up front, often after being burned by a rigid design. The anticipated change rarely arrives in the shape that was predicted, so the abstraction ends up both unused and wrong. AI agents are prone to this smell too, because generating extra layers costs them nothing.

Example

A notification module was built for "any channel" but only ever sends email.

notify.ts
interface Channel<TPayload> {
  send(payload: TPayload, options?: { priority?: number; retry?: boolean }): Promise<void>;
}

abstract class BaseChannel<T> implements Channel<T> {
  abstract send(payload: T, options?: { priority?: number; retry?: boolean }): Promise<void>;
}

class EmailChannel extends BaseChannel<{ to: string; body: string }> {
  async send(p: { to: string; body: string }) {
    await smtp.send(p.to, p.body);
  }
}

class ChannelRegistry {
  private channels = new Map<string, Channel<any>>();
  register(name: string, c: Channel<any>) { this.channels.set(name, c); }
  get(name: string) { return this.channels.get(name)!; }
}

const registry = new ChannelRegistry();
registry.register("email", new EmailChannel());
await registry.get("email").send({ to: "a@example.com", body: "Your order shipped" });

declare const smtp: { send(to: string, body: string): Promise<void> };

The priority and retry options are never read. Collapse to what is used:

notify.ts
export async function sendEmail(to: string, body: string): Promise<void> {
  await smtp.send(to, body);
}

await sendEmail("a@example.com", "Your order shipped");

declare const smtp: { send(to: string, body: string): Promise<void> };

When SMS arrives, you will know its real shape and can introduce an abstraction that fits both.

Treatment

  • If an abstract class has one subclass, use Collapse Hierarchy.
  • If a class only delegates or wraps without adding value, use Inline Class.
  • If a method only forwards to another, use Inline Method.
  • If a parameter is unused or always the same value, use Remove Parameter.
  • If a method has an abstract, future-proof name, use Rename Method so it says what it does today.
  • Delete tests that only exist to exercise the unused generality.

Payoff

  • Code says what the system does, not what it might do.
  • Fewer types and indirections to follow.
  • When the real second case arrives, you design the abstraction from two concrete examples instead of guessing.

When to ignore

  • You are building a framework or library whose users supply the other implementations.
  • The interface is a test seam: tests substitute a fake, and that fake counts as a second implementation.
  • The second implementation is committed work in the current iteration, not a "someday".
  • The abstraction isolates a third-party dependency you are contractually or operationally likely to swap.

Agent checklist

  • For each abstraction you removed, you confirmed there is no second implementation in tests, other packages or plugins.
  • Parameters you removed were unused or constant at every call site.
  • You did not delete an extension point that is part of a public API.
  • The simplified code passes the same tests, minus tests that only covered the removed generality.
  • You did not add new "for later" flexibility while simplifying.

Treatments

See also

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