Bài 4 · Nâng cao · 20 phút· Cập nhật 11/06/2026
L - Liskov Substitution
Biên soạn bởi Nguyễn Anh Tuấn
Liskov Substitution Principle (LSP): lớp con thay được lớp cha mà không làm thay đổi thuộc tính, hành vi mong muốn nào của chương trình
Liskov Substitution Principle (LSP): ở bất cứ đâu dùng lớp cha, ta phải thay được bằng lớp con mà chương trình vẫn chạy đúng. Nghe hiển nhiên, nhưng kế thừa "theo đời thường" hay phá nó:
❌ Vi phạm LSP - Square phá kỳ vọng của Rectangle
class Rectangle {
protected width = 0;
protected height = 0;
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
// Ep hai canh luon bang nhau -> pha tinh "doc lap" cua width/height
setWidth(w: number) { this.width = w; this.height = w; }
setHeight(h: number) { this.width = h; this.height = h; }
}
function printArea(r: Rectangle) {
r.setWidth(5);
r.setHeight(10);
// Ky vong: 5 * 10 = 50
console.log(r.area());
}
printArea(new Rectangle()); // 50 ✅
printArea(new Square()); // 100 ❌ -> thay the lam SAI ket qua - ▸Rectangle hứa: width và height đặt được ĐỘC LẬP.
- ▸Square phá lời hứa đó (đặt cạnh này kéo theo cạnh kia).
- ▸Hàm dùng Rectangle bỗng cho kết quả sai khi nhận Square → vi phạm LSP.
LSP là một hợp đồng (contract). Lớp con được phép làm nhiều hơn, nhưng không được phá những gì lớp cha đã hứa với người dùng. Bốn quy tắc:
- ▸Không siết chặt precondition (đầu vào) hơn lớp cha.
- ▸Không nới lỏng postcondition (đầu ra/cam kết) so với lớp cha.
- ▸Giữ nguyên các invariant (bất biến) của lớp cha.
- ▸Không ném ngoại lệ mới mà người dùng lớp cha không lường trước.
"is-a" đời thường ≠ "is-a" hành vi
Đừng ép Square là Rectangle. Cho cả hai là Shape với hợp đồng tối thiểu mà cả hai đều giữ được:
✅ Tuân thủ LSP - abstraction đúng mức
abstract class Shape {
abstract area(): number; // hop dong: moi Shape deu tinh duoc dien tich
}
class Rectangle extends Shape {
constructor(private w: number, private h: number) { super(); }
area() { return this.w * this.h; }
}
class Square extends Shape {
constructor(private side: number) { super(); }
area() { return this.side * this.side; }
}
// Thay the thoai mai: moi Shape giu dung hop dong area()
function totalArea(shapes: Shape[]): number {
return shapes.reduce((sum, s) => sum + s.area(), 0);
}
totalArea([new Rectangle(5, 10), new Square(4)]); // 50 + 16 = 66 ✅ - ▸Hợp đồng chung (area()) đủ nhỏ để mọi lớp con giữ trọn.
- ▸Không còn setWidth/setHeight gây mâu thuẫn - bỏ được "cái bẫy".
- ▸Mọi Shape thay thế nhau được ở totalArea() - đúng tinh thần LSP.
Một vi phạm hay gặp khác: Penguin extends Bird rồi buộc phải override fly() để ném lỗi "không bay được". Đó là tín hiệu cây kế thừa sai - hãy tách khả năng bay ra:
Tách hành vi thay vì ném lỗi
interface Bird { eat(): void; }
interface Flyable { fly(): void; }
class Sparrow implements Bird, Flyable {
eat() {}
fly() {}
}
class Penguin implements Bird { // khong implement Flyable
eat() {}
} Mùi của vi phạm LSP
Câu hỏi thường gặp
Barbara Liskov (1987): nếu S là kiểu con của T, thì ở mọi nơi dùng T, ta thay bằng S mà chương trình vẫn đúng. Nói nôm na: lớp con phải GIỮ LỜI HỨA của lớp cha - không làm người dùng lớp cha bất ngờ.
Đúng trong toán học, nhưng sai về HÀNH VI khi Rectangle cho phép đặt width và height ĐỘC LẬP. Square buộc hai cạnh bằng nhau nên phá kỳ vọng "đặt width=5, height=10 thì diện tích=50". "is-a" trong đời thường không bảo đảm thay thế được về hành vi.
Lớp con: (1) KHÔNG siết chặt điều kiện đầu vào (precondition) hơn cha; (2) KHÔNG nới lỏng cam kết đầu ra (postcondition) so với cha; (3) GIỮ các bất biến (invariant) của cha; (4) không ném ngoại lệ mới mà người dùng cha không lường. Vi phạm bất kỳ điều nào → có thể vỡ LSP.
Lớp con override một method rồi NÉM lỗi "không hỗ trợ"; hoặc để trống (no-op); hoặc code dùng phải kiểm tra "if (x instanceof SubType)" để xử lý ngoại lệ. Đó là lúc cây kế thừa sai - nên tách abstraction lại.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Liskov Substitution Principle nói gì?
- 1
Giải thích cái bẫy
Dùng ví dụ
Square/Rectangleở Bước 1: viết ra vì saoprintArea()đúng vớiRectanglenhưng sai vớiSquare.✅ Hoàn thành khi: Chỉ ra kỳ vọng bị phá (
widthvàheightđáng lẽ độc lập) và hậu quả (diện tích sai). - 2
Soi bằng hợp đồng
Lấy một cây kế thừa trong code của mèo con. Với một method, ghi precondition/postcondition của lớp cha rồi kiểm lớp con có giữ không.
✅ Hoàn thành khi: Chỉ ra ít nhất một method và kết luận có/không vi phạm, kèm lý do theo contract.
- 3
Sửa cây kế thừa
Refactor một vi phạm LSP (vd
Penguin extends Birdbuộc overridefly()ném lỗi) thành thiết kế đúng.✅ Hoàn thành khi: Lớp con không còn method "ném lỗi/no-op"; thay bằng tách interface (vd
Flyable) hoặc abstraction khác. - 4
is-a thật hay giả?
Nêu hai cặp lớp mà "is-a" đời thường đúng nhưng "thay thế hành vi" lại sai (ngoài
Square/Rectangle).✅ Hoàn thành khi: Hai ví dụ hợp lý; giải thích kỳ vọng nào của lớp cha bị lớp con phá.