Skip to content
aipatterns.dev

Code smellObject-Orientation Abusers

Refused Bequest

A subclass inherits methods or data it does not want, overriding them to throw or do nothing, so the hierarchy lies about what it can do.

Signs

  • A subclass overrides inherited methods to throw NotSupportedError or return without doing anything.
  • Callers check instanceof before calling a base-class method, because some subclasses can't honor it.
  • The subclass ignores most inherited fields; they sit unused on every instance.
  • The inheritance was chosen to reuse a few helper methods, not because the subclass "is a" kind of the parent.

Why it happens

Inheritance is the quickest way to reuse code: extend a class and its methods are yours. When the new class needs only part of the parent, the rest gets stubbed out. The hierarchy then claims a substitutability that does not hold, which breaks callers that trust the base type.

Example

A read-only archive storage that extends a general-purpose file store.

storage.ts
class FileStore {
  constructor(protected bucket: string) {}
  async read(key: string): Promise<Buffer> { /* ... */ return Buffer.alloc(0); }
  async list(prefix: string): Promise<string[]> { /* ... */ return []; }
  async write(key: string, data: Buffer): Promise<void> { /* ... */ }
  async delete(key: string): Promise<void> { /* ... */ }
}

class ArchiveStore extends FileStore {
  async write(): Promise<void> {
    throw new Error("archive is read-only");
  }
  async delete(): Promise<void> {
    throw new Error("archive is read-only");
  }
}

async function cleanup(store: FileStore) {
  for (const key of await store.list("tmp/")) await store.delete(key); // explodes for ArchiveStore
}

Splitting the capability into a narrower interface and using delegation makes the type honest.

storage.ts
interface ReadableStore {
  read(key: string): Promise<Buffer>;
  list(prefix: string): Promise<string[]>;
}

interface WritableStore extends ReadableStore {
  write(key: string, data: Buffer): Promise<void>;
  delete(key: string): Promise<void>;
}

class FileStore implements WritableStore { /* unchanged */ }

class ArchiveStore implements ReadableStore {
  constructor(private readonly inner: FileStore) {}
  read(key: string) { return this.inner.read(key); }
  list(prefix: string) { return this.inner.list(prefix); }
}

async function cleanup(store: WritableStore) { /* ArchiveStore no longer type-checks here */ }

Treatment

  • If the subclass is not truly a kind of the parent and only wants some of its behavior, use Replace Inheritance with Delegation. Hold the former parent as a field and forward only what you need.
  • If inheritance is appropriate but the parent has members only some children use, create a sibling class and use Push Down Method and Push Down Field to move those members into the children that want them.
  • If two classes share a genuine core and differ elsewhere, use Extract Superclass for the shared core and keep the rest separate.
  • Split wide interfaces by capability (read, write, admin) so each implementation declares only what it supports.

Payoff

  • Any object passed as the base type actually supports the base type's operations.
  • instanceof checks and runtime "not supported" errors disappear.
  • The compiler rejects misuse that previously failed in production.
  • The hierarchy documents real relationships instead of code-sharing shortcuts.

When to ignore

  • The refused method is rarely called and the parent is a framework class you cannot change; a documented no-op may be the least bad option.
  • The subclass refuses an implementation but still honors the interface, for example by overriding a method with a different but valid behavior. That is ordinary specialization.
  • The standard library does it deliberately, such as read-only collection views that throw on mutation, and callers are used to it. Match the convention of your platform.

Agent checklist

  • You listed every inherited member the subclass overrides to throw, ignore or leave unused.
  • You checked callers that accept the base type and confirmed which ones would break with the subclass.
  • After the change, no method in the hierarchy exists only to throw "not supported".
  • Callers that need the narrower capability accept the narrower type.
  • Tests pass, and a type-level or unit test shows the invalid call is now rejected.

Treatments

See also

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