Liskov Substitution
Liskov Substitution demands that subtypes honor the base contract.
Introduction
Liskov Substitution demands that subtypes honor the base contract. If Square extends Rectangle but changing width also changes height, callers expecting Rectangle behavior break silently. LSP failures hide in callers — the subtype looks fine in isolation.
The story
A drawing app used Square extends Rectangle. It normalized widths with setWidth(10) on every shape. Squares changed height too, breaking layouts subtly. Fix: both implement Shape; neither extends the other.
The business problem
LSP violations cause silent production bugs:
- Runtime surprises in callers using base-type references.
- Tests that lie — subtypes tested directly, not through base contract.
- False taxonomy — ISA logic without behavioral substitutability.
- NotSupportedException in "implementations" of interface methods.
The problem teams faced
Red flags for LSP:
- Subtypes throw on methods the base defines.
- Subtypes weaken preconditions or strengthen postconditions.
- Inheritance chosen for reuse, not true IS-A behavior.
Understanding the topic
LSP contract checklist for every subtype:
- Honors all base methods without surprising exceptions.
- Preserves invariants callers rely on.
- Passes tests written against base-type references.
Internal architecture
Caller perspective:
function layout(shapes: Rectangle[]) {shapes.forEach(s => s.setWidth(100)); // must not break any element}// Square in this list → LSP violation if width/height coupled
Visual explanation
LSP in three diagrams:
Informative example
Read-only list LSP violation — common in legacy APIs:
interface List<T> {add(item: T): void;get(i: number): T;}class ReadOnlyList<T> implements List<T> {add() { throw new Error("Not supported"); } // ❌ LSP violation}// ✅ Fix: separate interfacesinterface ReadableList<T> { get(i: number): T; }interface WritableList<T> extends ReadableList<T> { add(item: T): void; }
Execution workflow
Document base contract
Methods, pre/postconditions, invariants.
Real-world use
Java Collections documents LSP-borderline types explicitly. Modern APIs prefer small interfaces (ISP + LSP together) over fat bases with throwing overrides.
Production case study
Rectangle/Square in drawing pipeline (above):
- Symptom: intermittent layout bugs in production exports.
- Fix: drop inheritance; shared Shape interface.
- Lesson: ISA logic misleads — test substitutability.
Trade-offs
- Honest taxonomy prevents caller surprises.
- Some "obvious" hierarchies must flatten.
- Contract documentation adds upfront cost.
Decision framework
- Pause before every `extends` — run LSP checklist.
- Prefer composition when behavioral overlap is partial.
Best practices
- Contract tests for public interfaces.
- Throwing from override = red flag in review.
Anti-patterns to avoid
- Empty override that throws NotSupportedException.
- Square extends Rectangle because geometry class said so.
Common mistakes
- Testing subtypes only directly — misses LSP breaks.
Debugging tips
- Bug only when passed as base type? Classic LSP — audit subtype contract.
Optimization strategies
- Sealed classes + pattern matching can replace fragile inheritance (Visitor alternative).
Common misconceptions
- LSP applies to interfaces too — any contract, not only inheritance.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionReal LSP violation?+
Answer
Follow-up
2AdvancedQuestionEnforce LSP in review?+
Answer
Follow-up
Summary
You can audit LSP with contract tests, fix Square/Rectangle with composition, and catch throwing overrides in review.