CodePark/ questions / Inheritance, overriding & Liskov
START PRACTICINGPRACTICE
Backend question · Object-oriented programming › Inheritance, overriding & Liskov

What is the fragile base class problem? Explain the bug in this code: saveMany([a, b]) gives log === 4.

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: AuditedRepository holds 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 save is counted by both overrides, so two items give a count of 4.
  • Wrapping the repository with composition removes the hidden dependency.