VieTopik
Tiếng HànTiếng Anh.NET (C#)Java
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
OOP và thiết kế (Java)SOLID
Bài 10/12
5 phút

LSP — Class con phải thay được class cha

Nội dung bài · 6 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Sửa bằng interface
  4. 4.Thử ngay
  5. 5.Lỗi hay gặp
  6. 6.Tóm tắt

Vòng lặp hoàn tiền chạy ổn với mọi hình thức thanh toán, cho tới khi có thêm thẻ quà tặng. Class thẻ quà tặng kế thừa Payment nhưng refund() lại ném exception, vì thẻ quà tặng không hoàn tiền được. Chương trình sập ở chỗ không ai ngờ, và LSP giúp tránh loại lỗi này.

Khái niệm

🦆 LSP (Liskov Substitution Principle): object của class con phải dùng thay được cho object của class cha mà chương trình vẫn chạy đúng.

Class con không được làm ít hơn những gì class cha đã hứa. Class cha có refund() thì mọi class con đều phải hoàn tiền được.

Ví dụ

// SAI — GiftCardPayment không làm được refund
// Main.java
void main() {
    List<Payment> payments = List.of(
        new CardPayment(),
        new GiftCardPayment());

    for (Payment p : payments) {
        p.refund();   // sập ở GiftCardPayment
    }
}

// Payment.java
abstract class Payment {
    public abstract void refund();
}

// CardPayment.java
class CardPayment extends Payment {
    @Override
    public void refund() {
        System.out.println("Hoàn về thẻ");
    }
}

// GiftCardPayment.java
class GiftCardPayment extends Payment {
    @Override
    public void refund() {
        throw new UnsupportedOperationException(
            "Thẻ quà tặng không hoàn được");
    }
}
  • Vòng lặp tin rằng mọi Payment đều hoàn tiền được, vì class cha đã hứa như vậy.
  • GiftCardPayment phá lời hứa đó bằng UnsupportedOperationException (exception có sẵn báo thao tác không được hỗ trợ), nên thay nó vào chỗ Payment là chương trình sập.
  • Lỗi không nằm ở vòng lặp, mà ở chỗ GiftCardPayment không nên có refund().

Sửa bằng interface

Chỉ class nào hoàn tiền được mới implement Refundable, như ở bài Interface hay abstract class:

// Main.java
void main() {
    List<Refundable> refunds = List.of(
        new CardPayment());

    for (Refundable r : refunds) {
        r.refund();
    }
}

// Payment.java
abstract class Payment {
    private long amount;

    public long getAmount() {
        return amount;
    }

    public void setAmount(long amount) {
        this.amount = amount;
    }
}

// Refundable.java
interface Refundable {
    void refund();
}

// CardPayment.java
class CardPayment extends Payment
        implements Refundable {
    @Override
    public void refund() {
        System.out.println("Hoàn về thẻ");
    }
}

// GiftCardPayment.java
// Không hứa hoàn tiền, nên không vào được list trên
class GiftCardPayment extends Payment {
}

Thử ngay

Tạo các file của khối SAI ở phần Ví dụ, đổi chỗ hai phần tử trong list để new GiftCardPayment() đứng đầu, rồi chạy java Main.java.

Đoán trước khi chạy: dòng "Hoàn về thẻ" có được in ra không?

Xem kết quả
Exception in thread "main" java.lang.UnsupportedOperationException: Thẻ quà tặng không hoàn được

Không. Vòng lặp sập ngay ở phần tử đầu, nên CardPayment chưa kịp hoàn tiền. Lỗi kiểu này chỉ lộ ra khi dữ liệu thật có thẻ quà tặng, nên rất khó phát hiện sớm.

Lỗi hay gặp

Override chỉ để ném exception. Đây là dấu hiệu rõ nhất của vi phạm LSP.

// SAI — nhận tại cửa hàng thì không có mã vận đơn
// Shipment.java
abstract class Shipment {
    public abstract String track();
}

// StorePickup.java
class StorePickup extends Shipment {
    @Override
    public String track() {
        throw new UnsupportedOperationException();
    }
}

Nếu class con không làm được một việc của class cha, thì nó không phải là một loại của class cha. Hãy dùng interface nhỏ hơn hoặc composition.

// ĐÚNG — chỉ loại có mã vận đơn mới hứa track
// Shipment.java
abstract class Shipment {
    private String address = "";

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

// Trackable.java
interface Trackable {
    String track();
}

// HomeDelivery.java
class HomeDelivery extends Shipment
        implements Trackable {
    @Override
    public String track() {
        return "VN123456";
    }
}

// StorePickup.java
class StorePickup extends Shipment {
}

Tóm tắt

  • LSP: class con phải thay được class cha mà chương trình vẫn chạy đúng.
  • Class con không được làm ít hơn những gì class cha đã hứa.
  • Override chỉ để ném UnsupportedOperationException là dấu hiệu vi phạm.
  • Cách sửa: tách khả năng ra interface riêng, chỉ class làm được mới implement.

Tự kiểm tra

0/3 câu
Câu 1

Class Bird có method fly(). Class Penguin extends Bird override fly() bằng cách ném exception. Vấn đề là gì?

Câu 2

Cách sửa hợp lý cho ví dụ Bird và Penguin?

Câu 3

Dấu hiệu nào cho thấy có thể đang vi phạm LSP?

OCP — Mở rộng mà không sửa code cũISP — Interface nhỏ, đúng việc

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Sửa bằng interface
  4. 4.Thử ngay
  5. 5.Lỗi hay gặp
  6. 6.Tóm tắt