LSP — Class con phải thay được class cha
Nội dung bài · 6 mục
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. GiftCardPaymentphá lời hứa đó bằngUnsupportedOperationException(exception có sẵn báo thao tác không được hỗ trợ), nên thay nó vào chỗPaymentlà chương trình sập.- Lỗi không nằm ở vòng lặp, mà ở chỗ
GiftCardPaymentkhô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 đượcKhô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
UnsupportedOperationExceptionlà 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âuClass Bird có method fly(). Class Penguin extends Bird override fly() bằng cách ném exception. Vấn đề là gì?
Cách sửa hợp lý cho ví dụ Bird và Penguin?
Dấu hiệu nào cho thấy có thể đang vi phạm LSP?