LSP: bản chất của nguyên lý thay thế Barbara Liskov trong phát triển phần mềm

Tác giả: IT Sectr Đã đăng: 2026-05-12 Thời gian đọc: 9 phút

LSP (Liskov Substitution Principle) là nguyên lý thứ ba của SOLID, xác định các điều kiện cho kế thừa đúng đắn trong lập trình hướng đối tượng. Nguyên lý được Barbara Liskov xây dựng vào năm 1987 và được chính thức hóa như sau: nếu S là kiểu con của T, thì các đối tượng kiểu T có thể được thay thế bằng các đối tượng kiểu S mà không làm thay đổi các thuộc tính của chương trình. Như được nêu trong cuốn sách của Robert Martin Clean Architecture (2017), nguyên lý thay thế yêu cầu rằng lớp con không được làm yếu hợp đồng của lớp cơ sở.

Điểm chính

  • LSP — nguyên lý thay thế Liskov, nguyên lý SOLID thứ ba về kế thừa đúng đắn
  • Lớp con phải duy trì hợp đồng của lớp cơ sở — tiền điều kiện và hậu điều kiện
  • Vi phạm LSP thể hiện ở «vấn đề hình vuông và hình chữ nhật» và các ngoại lệ được ném ra
  • Hợp thành thường được ưu tiên hơn kế thừa để tuân thủ LSP
  • Thiết kế theo hợp đồng (Design by Contract) — cách chính thức để kiểm tra LSP

LSP (Liskov Substitution Principle) là gì?

LSP (Liskov Substitution Principle) là nguyên lý thay thế được Barbara Liskov xây dựng tại hội nghị OOPSLA năm 1987. Định nghĩa chính thức: gọi q(x) là một thuộc tính có thể chứng minh được của các đối tượng x thuộc kiểu T. Khi đó q(y) phải có thể chứng minh được cho các đối tượng y thuộc kiểu S, trong đó S là kiểu con của T. Nói cách khác: các đối tượng của lớp con phải hành xử sao cho mã làm việc với lớp cơ sở tiếp tục hoạt động chính xác với lớp con.

Trong thực tế, LSP có nghĩa là lớp con không được vi phạm hợp đồng của lớp cơ sở. Hợp đồng bao gồm tiền điều kiện (những gì cần để gọi một phương thức), hậu điều kiện (những gì được đảm bảo sau khi gọi) và bất biến (các điều kiện duy trì trong suốt vòng đời của đối tượng). Lớp con có thể tăng cường tiền điều kiện hoặc làm yếu hậu điều kiện — đó chính là vi phạm LSP.

Một ví dụ kinh điển về vi phạm LSP là hình vuông kế thừa từ hình chữ nhật. Phương thức setWidth của hình chữ nhật thiết lập chiều rộng, trong khi ở hình vuông nó thiết lập cả chiều rộng và chiều cao. Một khách hàng mong đợi hành vi của hình chữ nhật (thay đổi một cạnh không ảnh hưởng đến cạnh kia) sẽ nhận được kết quả bất ngờ. Hình vuông không phải là kiểu con hợp lệ của hình chữ nhật.

Các điều kiện chính thức của LSP

LSP thiết lập ba điều kiện cho kế thừa đúng đắn: tiền điều kiện của lớp con không thể mạnh hơn tiền điều kiện của lớp cơ sở (lớp con không đòi hỏi thêm), hậu điều kiện của lớp con không thể yếu hơn hậu điều kiện của lớp cơ sở (lớp con không đảm bảo ít hơn) và các bất biến của lớp cơ sở phải được duy trì trong lớp con. Các điều kiện này được gọi là quy tắc Thiết kế theo Hợp đồng của Bertrand Meyer.

Nếu ít nhất một điều kiện bị vi phạm, mã sử dụng đa hình có thể thất bại. Trình biên dịch không kiểm tra các hợp đồng ngữ nghĩa, chỉ kiểm tra các hợp đồng cú pháp. Do đó, LSP là vấn đề về kỷ luật kiến trúc, không phải là vấn đề về kiểu tĩnh.

Nguyên lý thay thế Liskov hoạt động như thế nào

Cơ chế LSP dựa trên tính tương thích hành vi của các kiểu. Nếu lớp S kế thừa từ lớp T, mã khách hàng phải có thể sử dụng S ở bất cứ nơi nào mong đợi T mà không thay đổi hành vi của nó. Điều này bao gồm không chỉ chữ ký phương thức mà cả ngữ nghĩa của chúng.

LSP không cấm lớp con thêm hành vi mới. Nhưng việc vi phạm kỳ vọng của mã được viết cho lớp cơ sở là bị cấm. Nếu lớp cơ sở đảm bảo rằng phương thức save không ném ngoại lệ, lớp con không được ném chúng. Nếu lớp cơ sở trả về giá trị không âm, lớp con không được trả về giá trị âm.

Trong các dự án thực tế, LSP thường bị vi phạm nhất khi thêm logic điều kiện vào các phương thức của lớp con: «nếu điều kiện — ném ngoại lệ», «nếu điều kiện — trả về null». Mỗi «bất ngờ» như vậy làm suy yếu tính đa hình và buộc mã khách hàng phải kiểm tra kiểu đối tượng trước khi gọi — điều này mâu thuẫn với chính ý tưởng của thiết kế hướng đối tượng.

Trong các dự án di động, một vi phạm LSP điển hình xảy ra khi tạo ViewModel cơ sở. Nếu BaseViewModel đảm bảo rằng phương thức onCleared giải phóng tất cả tài nguyên, và một lớp con ghi đè phương thức này thành rỗng — bất kỳ mã nào phụ thuộc vào việc dọn dẹp tài nguyên thông qua lời gọi đa hình onCleared sẽ hoạt động không chính xác. LSP yêu cầu lớp con gọi super.onCleared() hoặc tự thực hiện công việc tương tự. Hợp thành qua LifecycleObserver là một giải pháp thay thế loại bỏ vi phạm LSP trong quản lý vòng đời.

Dấu hiệu vi phạm LSP trong mã

Các chỉ báo chính của vi phạm LSP bao gồm: kiểm tra kiểu đối tượng qua instanceof hoặc is trước khi gọi phương thức, triển khai phương thức rỗng (stub), ném NotImplementedError hoặc UnsupportedOperationException, trả về null thay vì giá trị. Mỗi mẫu này báo hiệu rằng lớp con không phải là kiểu con hợp lệ.

Một dấu hiệu phổ biến khác là kế thừa nhằm mục đích tái sử dụng mã thay vì mô hình hóa mối quan hệ «là-một» (is-a). Lớp Bird có phương thức fly(). Lớp Penguin kế thừa từ Bird và ghi đè fly() thành rỗng hoặc ném ngoại lệ. Đây là vi phạm LSP: chim cánh cụt không phải là kiểu con hợp lệ của chim.

Trong phát triển di động, LSP bị vi phạm khi tạo các lớp cơ sở ViewHolder, Fragment hoặc ViewController với các phương thức stub. Nếu một lớp con không sử dụng một nửa số phương thức của lớp cơ sở — kế thừra đã được chọn sai. Hợp thành hoặc phân tách giao diện giải quyết vấn đề chính xác hơn.

Kiểm tra LSP

Một bài kiểm tra đơn giản để kiểm tra LSP: hãy viết một bài kiểm tra đơn vị cho lớp cơ sở để xác minh hợp đồng của nó (giá trị trả về, ngoại lệ, tác dụng phụ). Chạy bài kiểm tra này cho mỗi lớp con. Nếu bài kiểm tra thất bại — LSP đã bị vi phạm. Cách tiếp cận này được gọi là «kiểm tra qua hợp đồng của lớp cơ sở».

Trong các dự án Android, bài kiểm tra như vậy hữu ích cho ViewModel và Repository. Nếu BaseViewModel đảm bảo trạng thái Loading trước khi có lỗi, và một lớp con ném lỗi mà không có Loading — bài kiểm tra sẽ phát hiện vi phạm LSP ở giai đoạn CI.

Ví dụ LSP trong phát triển di động

Hãy xem ví dụ Android với xử lý ClickListener. Vi phạm LSP xảy ra khi triển khai cơ sở đảm bảo điều gì đó và lớp con vi phạm nó.

kotlin
// Lớp cơ sở với đảm bảo: onClick sẽ được gọi
open class BaseClickListener {
    open fun onClick(view: View) {
        // xử lý cơ bản
    }
}

// Vi phạm LSP: lớp con thêm điều kiện ném ngoại lệ
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Giải pháp đúng: hợp đồng không bị vi phạm
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Ví dụ iOS với giao thức DataSource thể hiện vi phạm LSP bằng cách trả về nil thay vì dữ liệu:

swift
// Giao thức với hợp đồng: trả về dữ liệu hoặc lỗi
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Vi phạm LSP: trả về nil mà không có lỗi
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // mảng rỗng thay vì lỗi
    }
}

// Tuân thủ LSP đúng
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Quy tắc thực tế: nếu một lớp con không thể thực hiện hợp đồng của lớp cơ sở, nó không nên là lớp con. Một giải pháp thay thế là trích xuất một giao diện với hợp đồng tối thiểu và triển khai nó trong mỗi kiểu theo cách riêng.

LSP và kế thừa: khi nào chọn hợp thành

Hợp thành được ưu tiên hơn kế thừa trong các tình huống mà mối quan hệ «là-một» (is-a) không rõ ràng hoặc có điều kiện. Một ví dụ kinh điển: Manager có phải là Employee không? Có. Nhưng Square có phải là Rectangle hợp lệ không? LSP nói «không». Nếu bạn nghi ngờ về tính đúng đắn của kế thừa — hãy chọn hợp thành.

Trong phát triển di động, hợp thành thường được sử dụng thông qua tiêm phụ thuộc: thay vì kế thừa hành vi từ một lớp cơ sở, một lớp nhận nó thông qua hàm tạo. ViewModel không kế thừa từ Repository mà chấp nhận nó như một phụ thuộc. Điều này loại bỏ vi phạm LSP theo định nghĩa — không kế thừa, không vi phạm hợp đồng.

Dấu hiệu cần thay thế kế thừa bằng hợp thành: lớp con không sử dụng một số phương thức của lớp cơ sở, lớp con ghi đè phương thức bằng stub rỗng, mã khách hàng kiểm tra kiểu đối tượng qua instanceof. Trong những trường hợp này, kế thừa đã được chọn sai và LSP đã bị vi phạm.

Giải pháp qua giao diện

Giao diện giải quyết vấn đề LSP mà không cần kế thừa: mỗi kiểu triển khai chính xác các phương thức mình cần. Thay vì một lớp cơ sở Bird chung với phương thức fly() (nơi Penguin không bay được) — một giao diện Flyable mà chỉ các loài chim biết bay triển khai. Penguin triển khai Bird mà không có phương thức fly() — LSP không bị vi phạm.

Trong kiến trúc Android, cách tiếp cận này được áp dụng thông qua các giao diện UseCase phân tách: thay vì một UseCase lớn với các phương thức getAll, getById, save, delete — các giao diện riêng biệt GetItemsUseCase, SaveItemUseCase. Khách hàng chỉ phụ thuộc vào giao diện cần thiết, và bất kỳ lớp nào triển khai giao diện đó đều đúng từ quan điểm LSP.

Câu hỏi thường gặp

LSP khác gì so với kế thừra đơn giản?

Kế thừa là một cơ chế ngôn ngữ; LSP là một quy tắc sử dụng đúng đắn cơ chế đó. Kế thừa đảm bảo tính tương thích chữ ký (cú pháp); LSP yêu cầu tính tương thích hành vi (ngữ nghĩa). Kế thừa không có LSP tạo ra tính đa hình bị hỏng khi chạy.

Null trong lớp con có luôn vi phạm LSP không?

Nếu lớp cơ sở đảm bảo trả về không null — có. Nếu hợp đồng cho phép null (giá trị tùy chọn) — không. LSP không cấm null; nó cấm làm yếu hợp đồng. Hãy nghiên cứu tài liệu của lớp cơ sở và kiểm tra xem hợp đồng của lớp con có tương thích không.

LSP áp dụng cho giao thức trong Swift như thế nào?

LSP áp dụng cho giao thức giống như cho lớp. Một triển khai giao thức phải tuân thủ hợp đồng ngữ nghĩa: nếu một giao thức định nghĩa phương thức là non-throwing, triển khai không được ném lỗi. Swift không kiểm tra điều này ở cấp trình biên dịch — trách nhiệm thuộc về nhà phát triển.

LSP có thể bị vi phạm khi sử dụng sealed class không?

Sealed class trong Kotlin là một trường hợp đặc biệt vì hệ phân cấp được đóng và trình biên dịch biết được. LSP áp dụng cho sealed class ở mức độ thấp hơn vì tất cả các kiểu con được liệt kê rõ ràng trong biểu thức when. Lỗi của một lớp con sealed sẽ là cục bộ, không phải lỗi đa hình ẩn.

Làm thế nào để kiểm tra sự tuân thủ LSP trong dự án?

Hãy viết một bài kiểm tra tham số hóa cho lớp cơ sở chạy cho tất cả các lớp con của nó. Bài kiểm tra xác minh các hợp đồng hành vi chính: giá trị trả về, ngoại lệ, trạng thái. Nếu bài kiểm tra thất bại trên một lớp con — LSP đã bị vi phạm. Trong CI, bài kiểm tra như vậy ngăn chặn sự thoái lui của mã đa hình.

Tổng kết

  • LSP (Liskov Substitution Principle) — nguyên lý thay thế, thứ ba trong SOLID, về tính tương thích ngữ nghĩa của kế thừa
  • Lớp con phải duy trì hợp đồng của lớp cơ sở: tiền điều kiện, hậu điều kiện và bất biến
  • Kiểm tra instanceof và ghi đè phương thức rỗng là dấu hiệu chính của vi phạm LSP
  • Hợp thành và giao diện giải quyết vấn đề LSP khi kế thừa không đúng
  • Vấn đề hình vuông và hình chữ nhật là ví dụ kinh điển về sự không tương thích kiểu con
  • Kiểm tra hợp đồng cho lớp cơ sở, chạy cho tất cả lớp con, phát hiện vi phạm LSP trong CI
  • Sealed class trong Kotlin giảm rủi ro LSP nhờ hệ phân cấp đóng mà trình biên dịch biết được

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm