bài viết ngẫu nhiên
If you’ve ever heard of SOLID, then the letter L stands for Liskov Substitution Principle.
The name sounds very academic, but at its core, it revolves around a very simple idea:
If a subclass inherits from a parent class, the subclass must be able to replace the parent class without breaking the program’s logic.
In other words:
Wherever the “parent” is used, you should be able to plug in the “child” and everything still works.
Imagine you have a Bird class with a fly() method.
That makes sense—birds can fly, right?
class Bird {
fly() {
console.log("I'm flying!");
}
}
Now you add another bird species — a sparrow:
class Sparrow extends Bird {
fly() {
console.log("Sparrow is flying high!");
}
}
So far, so good.
But then you add a penguin:
class Penguin extends Bird {
fly() {
throw new Error("Penguin cannot fly");
}
}
Now things get messy.
Your program’s logic assumes that all birds can fly. But once you pass in a penguin, the program breaks.
This is a classic example of violating LSP.
Instead of assuming that all birds can fly, we split the abstraction into clearer, more accurate ones.
interface Bird {
eat(): void;
}
interface FlyingBird extends Bird {
fly(): void;
}
class Sparrow implements FlyingBird {
eat() { console.log("Sparrow is eating"); }
fly() { console.log("Sparrow is flying"); }
}
class Penguin implements Bird {
eat() { console.log("Penguin is eating"); }
}
Now:
Sparrow is a bird that can fly
Penguin is a bird that cannot fly
The program no longer makes false assumptions
At first glance, this might sound like a simple story about “birds flying or not flying”, but in real code, LSP violations show up all the time:
Sometimes we create a too-general parent class, then force all subclasses to implement methods that don’t really make sense.
The result?throw new Error("Not supported") in subclasses — a clear LSP code smell.
If a subclass cannot cleanly replace its parent, writing unit tests becomes painful.
A test works fine with ClassA, but suddenly fails when switching to ClassB.
extends instead of compositionInheritance isn’t always the right choice.
In many cases, composition (class A has class B) is better than forcing A to extend B.
Suppose you’re building a payment processing service.
class Payment {
process(amount: number) {
console.log(`Processing ${amount}`);
}
}
class CreditCardPayment extends Payment {
process(amount: number) {
console.log(`Processing credit card payment: ${amount}`);
}
}
Looks fine. But then you add cash payments:
class CashPayment extends Payment {
process(amount: number) {
throw new Error("Cash cannot be processed like this");
}
}
Once again, LSP is violated.CashPayment does not truly fit the current Payment abstraction.
Split the abstraction properly:
interface Payment {
pay(amount: number): void;
}
class CreditCardPayment implements Payment {
pay(amount: number) {
console.log(`Paid ${amount} by credit card`);
}
}
class CashPayment implements Payment {
pay(amount: number) {
console.log(`Paid ${amount} in cash`);
}
}
Now everything works smoothly.
Whether you use CreditCardPayment or CashPayment, the program behaves correctly—no “Unsupported” errors.
Is any subclass throwing errors because a method is not applicable?
Does any logic assume “the parent can do X” while a child actually cannot?
Should the abstraction be split into multiple clearer interfaces?
Are you using extends just for convenience instead of composition?
Liskov Substitution Principle may sound academic, but it really just reminds us:
Don’t force a child to do what the parent can do if it doesn’t truly make sense.
Applying LSP makes your code more flexible, easier to test, and less prone to surprising bugs.
And most importantly—you’ll never end up with a penguin that can fly again. 🐧✈️