← SOLID Principles

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

Hình vuông LÀ hình chữ nhật trong toán học, nhưng về HÀNH VI (đặt cạnh độc lập) thì không. Trước khi cho B kế thừa A, hãy hỏi: "B có thay được A ở MỌI nơi dùng A không?" Nếu không → đừng kế thừa.

Đừ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

Khi bạn thấy lớp con override một method để ném lỗi "không hỗ trợ", để trống (no-op), hoặc code dùng phải if (x instanceof ...) để né trường hợp đặc biệt - gần như chắc chắn LSP đang bị phá. Hãy tách interface (liên hệ Bài 5 - ISP).

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 widthheight ĐỘ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.

Luyện nhận diện vi phạm: bài Tổng quan SOLID →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/5 Điểm: 0

Liskov Substitution Principle nói gì?

Bài tập về nhà

  1. 1

    Giải thích cái bẫy

    Dùng ví dụ Square/Rectangle ở Bước 1: viết ra vì sao printArea() đúng với Rectangle nhưng sai với Square.

    ✅ Hoàn thành khi: Chỉ ra kỳ vọng bị phá (widthheight đáng lẽ độc lập) và hậu quả (diện tích sai).

  2. 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. 3

    Sửa cây kế thừa

    Refactor một vi phạm LSP (vd Penguin extends Bird buộc override fly() 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. 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á.