Nguồn: https://dotnettutorials.net/lesson/liskov-substitution-principle/
Series: SOLID Design Principles trong C#
- Single Responsibility Principle (SRP) trong C#:
- Open-Closed Principle (OCP) trong C#:
- Liskov Substitution Principle (LSP) trong C#:
Nguyên lý thay thế của Liskov (LSP), là nguyên lý thứ ba trong bộ nguyên lý SOLID, được giới thiệu bởi Barbara Liskov.
🔸 LSP phát biểu rằng:
Đối tượng của một lớp cha phải có thể được thay thế bằng đối tượng của một lớp con mà không làm thay đổi tính đúng đắn của chương trình.
Nói cách khác, lớp con phải tuân thủ đầy đủ hành vi được mong đợi từ lớp cha, và không được làm thay đổi logic hoặc gây lỗi cho chương trình nếu thay thế lớp cha bằng lớp con. Nguyên lý này khuyến khích một thiết kế trong đó các lớp con có thể thay thế được cho lớp cha một cách trơn tru.
Ví dụ minh họa đơn giản:
Một người cha là giáo viên, còn người con trai là bác sĩ.
→ Trong trường hợp này, người con không thể thay thế vai trò của người cha, mặc dù cả hai cùng thuộc một gia đình.
→ Điều này cho thấy rằng người con không tuân thủ hành vi mà vai trò của người cha mong đợi → Vi phạm nguyên lý Liskov.
Ví dụ KHÔNG tuân theo Liskov Substitution Principle (LSP) trong C#
Trước hết, ta sẽ xem một ví dụ không tuân thủ nguyên lý Liskov trong C#. Sau đó, ta sẽ chỉ ra vấn đề xảy ra khi không tuân theo nguyên lý này và cuối cùng sẽ cải tiến lại để tuân thủ LSP.
Mô tả vấn đề:
Trong ví dụ dưới đây:
- Ta có một lớp
Apple
với phương thứcGetColor()
trả về"Red"
. - Lớp
Orange
kế thừa từApple
và ghi đè (override
) phương thứcGetColor()
để trả về"Orange"
. - Tuy nhiên, khi ta gán một đối tượng
Orange
cho biến tham chiếu kiểuApple
, thì ta lại nhận được kết quả"Orange"
chứ không phải"Red"
– điều này khiến hành vi không còn đúng với lớp chaApple
mong đợi.
using System;
namespace SOLID_PRINCIPLES.LSP
{ class Program { static void Main(string[] args) { Apple apple = new Orange(); Console.WriteLine(apple.GetColor()); } } public class Apple { public virtual string GetColor() { return "Red"; } } public class Orange : Apple { public override string GetColor() { return "Orange"; } }
}
Phân tích:
- Lớp
Orange
kế thừa từApple
, nên về mặt cú pháp, việcApple apple = new Orange();
là hợp lệ. - Tuy nhiên, về mặt hành vi,
Orange
không thực sự là mộtApple
, vì khi gọiGetColor()
thì kết quả là"Orange"
thay vì"Red"
. - Điều này vi phạm nguyên lý Liskov, vì hành vi của đối tượng bị thay đổi khi ta thay thế lớp cha bằng lớp con.
🔴 Nếu thay thế một Apple bằng một Orange mà khiến chương trình hiểu sai bản chất của Apple, thì rõ ràng có vấn đề trong thiết kế.
Ví dụ CÓ tuân theo Liskov Substitution Principle (LSP) trong C#
Bây giờ, ta sẽ chỉnh sửa lại ví dụ trước để tuân thủ nguyên lý Liskov trong C#. Thay vì để Orange
kế thừa Apple
, ta sẽ tạo một giao diện chung (IFruit
) mà cả Apple
và Orange
đều cùng triển khai (implement).
using System;
namespace SOLID_PRINCIPLES.LSP
{ class Program { static void Main(string[] args) { IFruit fruit = new Orange(); Console.WriteLine($"Color of Orange: {fruit.GetColor()}"); fruit = new Apple(); Console.WriteLine($"Color of Apple: {fruit.GetColor()}"); Console.ReadKey(); } } public interface IFruit { string GetColor(); } public class Apple : IFruit { public string GetColor() { return "Red"; } } public class Orange : IFruit { public string GetColor() { return "Orange"; } }
}
Output:
Color of Orange: Orange
Color of Apple: Red
Giải thích:
IFruit
là một giao diện đại diện cho mọi loại trái cây có chung hành vi GetColor()
.
Apple
và Orange
không phụ thuộc lẫn nhau mà chỉ triển khai IFruit
.
Do đó, khi ta thay fruit
bằng new Apple()
hoặc new Orange()
, hành vi không bị thay đổi – chương trình vẫn hiểu đúng bản chất của từng đối tượng.
Đây là tuân thủ hoàn toàn nguyên lý LSP vì:
“Chúng ta có thể thay thế đối tượng của lớp cha
IFruit
bằng đối tượng của lớp con (Apple
,Orange
) mà không ảnh hưởng đến tính đúng đắn của chương trình.”
Ghi nhớ quan trọng:
Việc sử dụng kế thừa không nên làm một cách tùy tiện.
Dù cú pháp có thể đúng và không gây lỗi, nhưng nếu hành vi bị sai lệch, thì đó là một thiết kế sai.
Vì vậy, hãy luôn tự hỏi:
- “Có hợp lý không nếu lớp con là một loại của lớp cha?”
- Nếu câu trả lời là “Không”, thì đừng dùng kế thừa — hãy xem xét dùng interface hoặc composition.
Ví dụ thực tế về Liskov Substitution Principle (LSP) trong C#: Tài khoản Ngân hàng
Ví dụ dưới đây mô phỏng hệ thống ngân hàng với các loại tài khoản như Tài khoản tiết kiệm và Tài khoản thanh toán. Qua đó, ta sẽ thấy cách nguyên lý Liskov Substitution được áp dụng đúng cách.
using System;
namespace LSPDemo
{ //Imagine you have a base class BankAccount public class BankAccount { public string AccountNumber { get; set; } public decimal Balance { get; set; } public BankAccount(string accountNumber, decimal balance) { AccountNumber = accountNumber; Balance = balance; } public virtual void Deposit(decimal amount) { Balance += amount; Console.WriteLine($"Deposit: {amount}, Total Amount: {Balance}"); } public virtual void Withdraw(decimal amount) { if (amount <= Balance) { Balance -= amount; } else { Console.WriteLine("Insufficient balance."); } } } //We have two derived classes: SavingsAccount and CurrentAccount public class SavingsAccount : BankAccount { public decimal InterestRate { get; set; } public SavingsAccount(string accountNumber, decimal balance, decimal interestRate) : base(accountNumber, balance) { InterestRate = interestRate; } public override void Withdraw(decimal amount) { if (amount <= Balance) { Balance -= amount; Console.WriteLine($"AccountNumber: {AccountNumber}, Withdraw: {amount}, Balance: {Balance}"); } else { Console.WriteLine($"AccountNumber: {AccountNumber}, Withdraw: {amount}, Insufficient Funds, Available Funds: {Balance}"); } } } public class CurrentAccount : BankAccount { public decimal OverdraftLimit { get; set; } public CurrentAccount(string accountNumber, decimal balance, decimal overdraftLimit) : base(accountNumber, balance) { OverdraftLimit = overdraftLimit; } public override void Withdraw(decimal amount) { if (amount <= Balance + OverdraftLimit) { Balance -= amount; Console.WriteLine($"AccountNumber: {AccountNumber}, Withdraw: {amount}, Balance: {Balance}"); } else { Console.WriteLine($"AccountNumber: {AccountNumber}, Exceeded Overdraft Limit."); } } } //Testing the Liskov Substitution Principle public class Program { public static void Main() { BankAccount savingsAccount = new SavingsAccount("SA123", 1000m, 0.03m); BankAccount currentAccount = new CurrentAccount("CA456", 1500m, 500m); Console.WriteLine("Before Transactions:"); PrintAccountDetails(savingsAccount); PrintAccountDetails(currentAccount); savingsAccount.Withdraw(500m); currentAccount.Withdraw(2000m); Console.WriteLine("\nAfter Transactions:"); PrintAccountDetails(savingsAccount); PrintAccountDetails(currentAccount); Console.ReadKey(); } static void PrintAccountDetails(BankAccount account) { Console.WriteLine($"Account Number: {account.AccountNumber}, Balance: {account.Balance}"); } }
}
Output:
Before Transactions:
Account Number: SA123, Balance: 1000
Account Number: CA456, Balance: 1500
Account Number: SA123, Withdraw: 500, Balance: 500
Account Number: CA456, Withdraw: 2000, Balance: -500 After Transactions:
Account Number: SA123, Balance: 500
Account Number: CA456, Balance: -500
Mô tả mã nguồn:
Lớp cơ sở BankAccount
:
- Có các thuộc tính chung như:
AccountNumber
,Balance
. - Cung cấp các phương thức chung:
Deposit()
để nạp tiền.Withdraw()
để rút tiền – mặc định không cho rút quá số dư.
Lớp con SavingsAccount
:
- Kế thừa từ
BankAccount
. - Thêm thuộc tính
InterestRate
(lãi suất). - Ghi đè
Withdraw()
để áp dụng quy tắc: không được rút vượt quá số dư.
Lớp con CurrentAccount
:
- Kế thừa từ
BankAccount
. - Thêm thuộc tính
OverdraftLimit
(hạn mức thấu chi). - Ghi đè
Withdraw()
để cho phép rút vượt quá số dư trong giới hạnOverdraftLimit
.
Ghi nhớ quan trọng:
🧠 Không phải lúc nào có kế thừa cũng nghĩa là đúng theo OOP.
Nếu việc kế thừa không phản ánh đúng bản chất "is-a" (là một loại của) thì sẽ dẫn đến sai lệch hành vi và vi phạm nguyên lý LSP.