class BaseRepository<T> {
save(item: T): void { /* insert */ }
saveMany(items: T[]): void { for (const i of items) this.save(i); }
}
class AuditedRepository<T> extends BaseRepository<T> {
log = 0;
override save(item: T): void { this.log++; super.save(item); }
override saveMany(items: T[]): void {
this.log += items.length;
super.saveMany(items);
}
}
The fragile base class problem: a change in a parent class, or a detail inside it, breaks a subclass, even though the parent's public API looks the same. The subclass depends on how the parent works, not only on what it promises.
The bug: saveMany in the base calls this.save() for each item. this is the AuditedRepository, so the overridden save runs and adds 1 per item. The subclass's saveMany has already added 2. Total: 4.
The author could not know this without reading the base code. Worse: if the base later inserts in bulk and stops calling save, the count changes again. No subclass code changed.
Fixes:
- Composition:
AuditedRepositoryholds a repository and wraps it. It doesn't extend it. Then it counts only its own calls. - Make the base class document which methods call which, or mark methods as not for overriding.
- Keep inheritance trees shallow and avoid overriding methods that other base methods call.
- A subclass can break when the base class's internal code changes.
- Overriding a method changes every base method that calls it through
this. - Here
saveis counted by both overrides, so two items give a count of 4. - Wrapping the repository with composition removes the hidden dependency.
Read more