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
lỗi, 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 — GiftCard kế thừa nhưng không làm được Refund
var payments = new List<Payment>
{
new CardPayment(),
new GiftCardPayment(),
};
foreach (Payment p in payments)
{
p.Refund(); // sập ở GiftCardPayment
}
abstract class Payment
{
public abstract void Refund();
}
class CardPayment : Payment
{
public override void Refund() =>
Console.WriteLine("Hoàn về thẻ");
}
class GiftCardPayment : Payment
{
public override void Refund()
{
throw new NotSupportedException(
"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ằngNotSupportedException(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 IRefundable, như ở bài
Interface hay abstract class:
var refunds = new List<IRefundable>
{
new CardPayment(),
};
foreach (IRefundable r in refunds)
{
r.Refund();
}
abstract class Payment
{
public decimal Amount { get; set; }
}
interface IRefundable
{
void Refund();
}
class CardPayment : Payment, IRefundable
{
public void Refund() =>
Console.WriteLine("Hoàn về thẻ");
}
// Không hứa hoàn tiền, nên không vào được list trên
class GiftCardPayment : Payment
{
}Thử ngay
Chép khối SAI ở phần Ví dụ vào Program.cs, đổi chỗ hai phần tử trong
list để new GiftCardPayment() đứng đầu, rồi chạy dotnet run.
Đoán trước khi chạy: dòng "Hoàn về thẻ" có được in ra không?
Xem kết quả
Unhandled exception. System.NotSupportedException: 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
abstract class Shipment
{
public abstract string Track();
}
class StorePickup : Shipment
{
public override string Track()
{
throw new NotSupportedException();
}
}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
abstract class Shipment
{
public string Address { get; set; } = "";
}
interface ITrackable
{
string Track();
}
class HomeDelivery : Shipment, ITrackable
{
public string Track() => "VN123456";
}
class StorePickup : 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
NotSupportedExceptionlà 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 : 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?