Liskov Substitution Principle (LSP)
Why can't a Penguin inherit from a Bird that flies? Master the Liskov Substitution Principle to build reliable and logical hierarchies.
The Liskov Substitution Principle (LSP) is the most technical of the SOLID principles. It states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
The Essentials
The "No Surprises" guide:
- Substitutability: A child class must be able to stand in for its parent perfectly.
- Behavioral Contract: A child shouldn't just have the same "name" as a parent; it must follow the parent's "logic."
- Unexpected Exceptions: If a child method throws a "NotImplemented" error for a parent's behavior, it's a violation.
- Correct Hierarchy: LSP forces you to think deeply about whether your inheritance is actually logical.
The Duck Test
If it looks like a duck and quacks like a duck, but needs batteries to work, you probably have the wrong abstraction.
- The Parent:
Duckwith aquack()method. - The Child:
ToyDuck. - The Violation: If you pass a
ToyDuckto a function expecting a realDuck, and the batteries are dead, the function fails. TheToyDuckis not a perfect substitute.
In code, this often happens with "Penguins" and "Birds." If your Bird class has a fly() method, and you make Penguin a child of Bird, you have an LSP violation because penguins cannot fly.
LSP in Action: The Substitution Problem
A Square inherits from Rectangle, but changes the behavior. Changing height now affects width, unexpectedly!
setHeight(h) {
this.h = h;
this.w = h; // Surprise!
}
}
A function expecting a generic Rectangle will be confused when width changes by itself. The substitute is "surprising."
We use a shared Shape interface. Both classes implement it correctly without breaking each other's rules.
class Rectangle implements Shape {
getArea() { return h * w; }
}
class Square implements Shape {
getArea() { return s * s; }
}
Now, any function using Shape can trust that getArea() works perfectly for every type.
Code Implementation: The Bird/Penguin Logic
The classic way to fix LSP is to move shared behavior to a common base and specific behavior (like flying) to a separate interface.
// The shared root for all birds
abstract class Bird {
abstract eat(): void;
}
interface FlyingBird {
fly(): void;
}
class Eagle extends Bird implements FlyingBird {
eat() { console.log("Eagle is eating"); }
fly() { console.log("Eagle is soaring high!"); }
}
class Penguin extends Bird {
eat() { console.log("Penguin is eating fish"); }
// Penguin DOES NOT implement FlyingBird.
// No more "surprises" for the fly() method!
}
// Usage
function feedBird(bird: Bird) {
bird.eat(); // Works for both Eagle and Penguin!
}Why It Matters: Trust
LSP is about Trust. When you write code that uses a User class, you should be able to trust that any type of user (Student, Admin, Guest) will behave exactly like a User.
- No Type Checking: You don't want to write
if (user is Admin) { ... }everywhere. - Modular Growth: You can add new child types with confidence, knowing they won't crash the existing systems that rely on the parent's contract.
By following LSP, you ensure that your inheritance trees are logically sound and that your code is truly polymorphic.
Next, we'll look at the Interface Segregation Principle and learn how to keep our contracts lean and mean.
Practice what you just read.