โ Back to OOP Concepts
๐
Principles
Liskov Substitution
Subtypes must be substitutable for their base types without altering the correctness of the program.
The Problem
If a subclass overrides behavior in ways that violate the expectations of its base class โ e.g., throwing for operations the base class supports, or returning unexpected results โ code that relies on the base type breaks when given the subtype.
How It Works
- โบA subclass should honor the contracts (preconditions, postconditions, invariants) of its superclass
- โบOverridden methods should accept at least as much input and return at least as little as the base method
- โบClassic violation: Square extends Rectangle but breaks the independence of setWidth/setHeight
- โบIf a subtype can't fully honor the base type's contract, reconsider the inheritance relationship
When to Use
โDesigning class hierarchies where subclasses must be drop-in replacements
โReviewing inheritance to check for surprising overridden behavior
โChoosing between inheritance and composition for an "almost is-a" relationship
Real-World Examples
๐
iOS
Any UIView subclass must work correctly anywhere a UIView is expected
๐ค
Android
Any RecyclerView.ViewHolder subclass must behave correctly within the adapter
๐
Web
Any Storage implementation (localStorage/sessionStorage) must support the same get/set/remove contract
Code Examples
Swift
// โ Violation: Penguin can't fly, breaking the Bird contract
// class Penguin: Bird { override func fly() { fatalError("can't fly") } }
// โ
Fix: separate flying ability into its own protocol
protocol Bird {
func eat()
}
protocol FlyingBird: Bird {
func fly()
}
struct Sparrow: FlyingBird {
func eat() { print("Sparrow eating") }
func fly() { print("Sparrow flying") }
}
struct Penguin: Bird {
func eat() { print("Penguin eating") }
// no fly() โ Penguin is not a FlyingBird, and that's fine
}
func feed(_ bird: Bird) { bird.eat() }
feed(Penguin()) // works correctly for any Bird