bài viết ngẫu nhiên
Nếu bạn từng nghe tới SOLID, thì chữ L chính là Liskov Substitution Principle (nguyên tắc thay thế Liskov). Nghe tên có vẻ “học thuật hàn lâm”, nhưng thực ra nó xoay q

uanh một ý rất đơn giản:
Nếu một class con kế thừa class cha, thì class con đó phải thay thế được class cha mà không làm hỏng logic chương trình.
Nói cách khác: chỗ nào dùng “ông bố”, thì đưa “đứa con” vào cũng chạy ổn.
Hãy thử hình dung bạn có một class Bird với method fly().
Tất nhiên rồi, chim thì bay được mà.
class Bird {
fly() {
console.log("I'm flying!");
}
}
Giờ bạn thêm một loài chim khác – chim sẻ:
class Sparrow extends Bird {
fly() {
console.log("Sparrow is flying high!");
}
}
Ổn áp. Nhưng rồi bạn thêm chim cánh cụt:
class Penguin extends Bird {
fly() {
throw new Error("Penguin cannot fly");
}
}
Tới đây thì chuyện rắc rối rồi. Bạn code logic dựa vào giả định rằng mọi con chim đều bay được. Nhưng đưa chim cánh cụt vào thì chương trình “toang”. Đây chính là một ví dụ điển hình vi phạm LSP.
Thay vì giả định tất cả các loài chim đều bay, ta chia abstraction thành nhiều loại rõ ràng hơn.
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"); }
}
Bây giờ:
Nghe qua có vẻ chỉ là chuyện “chim bay hay không bay”, nhưng trong code thực tế thì thường gặp ở những tình huống như:
Nhiều khi ta tạo ra một class cha “quá tổng quát”, rồi ép tất cả class con phải implement những method không phù hợp. Kết quả là phải throw new Error("Not supported") trong class con → đây chính là mùi code vi phạm LSP.
Nếu một class con không thể thay thế class cha một cách “êm đẹp”, bạn sẽ cực khổ khi viết unit test. Một test chạy ngon với ClassA nhưng fail ngay khi đổi sang ClassB.
Không phải lúc nào kế thừa cũng là lựa chọn tốt. Nhiều trường hợp chỉ cần composition (class A chứa class B) thay vì “ép buộc” A kế thừa B.
Giả sử bạn đang viết một service xử lý thanh toán.
class Payment {
process(amount: number) {
console.log(`Processing ${amount}`);
}
}
class CreditCardPayment extends Payment {
process(amount: number) {
console.log(`Processing credit card payment: ${amount}`);
}
}
Ổn. Nhưng nếu bạn thêm CashPayment:
class CashPayment extends Payment {
process(amount: number) {
throw new Error("Cash cannot be processed like this");
}
}
Lại vi phạm LSP rồi. Vì CashPayment không thực sự phù hợp với abstraction Payment hiện tại.
Thay vì thế, ta có thể tách abstraction hợp lý hơn:
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`);
}
}
Giờ thì ổn. Dù dùng CreditCardPayment hay CashPayment, chương trình vẫn chạy ổn, không phát sinh lỗi “Unsupported”.
Liskov Substitution Principle nghe thì học thuật, nhưng thực chất là nhắc ta:
Đừng ép một đứa con phải làm những gì ông bố có thể làm nếu nó thực sự không phù hợp.
Áp dụng LSP giúp code của bạn linh hoạt hơn, dễ test hơn, ít bug bất ngờ hơn. Và quan trọng là: bạn sẽ không còn rơi vào cảnh “chim cánh cụt biết bay” nữa.