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
NotSupportedErroror return without doing anything. - Callers check
instanceofbefore 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.
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.
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.
instanceofchecks 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
- Replace Inheritance with DelegationTurn a subclass that uses only part of its superclass into a class that holds an instance of it and forwards just the calls it needs.
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
- Push Down MethodMove a superclass method into the only subclasses that use it, so the superclass describes only what all subclasses share.
- Push Down FieldMove a superclass field into the subclasses that use it, so other subclasses stop carrying state they never touch.
See also
- Alternative Classes with Different InterfacesTwo classes do the same job but expose different method names or signatures, so callers cannot swap one for the other.
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
- Parallel Inheritance HierarchiesEvery new subclass in one hierarchy forces a matching subclass in another, so two trees must always grow in lockstep.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.