- vừa được xem lúc

Liskov Substitution Principle (LSP) trong C#

0 0 3

Người đăng: OssiLV

Theo Viblo Asia

Nguồn: https://dotnettutorials.net/lesson/liskov-substitution-principle/

Series: SOLID Design Principles 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ức GetColor() trả về "Red".
  • Lớp Orange kế thừa từ Appleghi đè (override) phương thức GetColor() để trả về "Orange".
  • Tuy nhiên, khi ta gán một đối tượng Orange cho biến tham chiếu kiểu Apple, 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 cha Apple 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ệc Apple apple = new Orange();hợp lệ.
  • Tuy nhiên, về mặt hành vi, Orange không thực sự là một Apple, vì khi gọi GetColor() 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ụ 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ả AppleOrange đề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().

AppleOrange 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.

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ệmTà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ạn OverdraftLimit.

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.

Bình luận

Bài viết tương tự

- vừa được xem lúc

4 đặc tính của lập trình hướng đối tượng (Object oriented program)

Lập trình hướng đối tượng quá quen thuộc rồi bạn nào học lập trình đều phải học, đi phỏng vấn cũng vậy hỏi suốt(chắc cái này tùy vào vị trí tuyển dụng chủ yếu junior chắc chắn sẽ hỏi).nó là nền tảng cho hầu hết các design pattern hiện nay.

0 0 52

- vừa được xem lúc

Khác nhau giữa abstract class và interface khi nào dùng chúng

Nhắc đến Interface và abstract class hãy nhớ 2 từ này khá clear rồi, Khi sử dụng Interface là bạn Implement còn sử dụng abstract class là bạn extend. . Interface:. .

0 0 47

- vừa được xem lúc

So sánh Interface và Abstract trong lập trình hướng đối tượng.

Tổng quan. Interface và Abstract class là 2 khái niệm cơ bản trong lập trình OOP.

0 0 67

- vừa được xem lúc

Áp Dụng Nguyên Tắc SOLID Trong Lập Trình

Giới Thiệu. 1. SOLID là gì. SOLID là viết tắt của 5 chữ cái đầu trong 5 nguyên tắc:.

0 0 42

- vừa được xem lúc

Kỹ thuật giải quyết bài toàn về policy và công thức tính toán động cho sản phẩm phần mềm

Dạo này tôi có một mối duyên rất tình cờ với việc làm các phần mềm thuộc lĩnh vực tài chính và ngân hàng. Một số bài toán trong lĩnh vực này làm tôi nhớ đến những ngày đầu làm việc với phần mềm Trinet

0 0 39

- vừa được xem lúc

Object Relational Mapping

Trong cách phát triển ứng dụng web hiện nay chắc hẳn các bạn đã quen với với từ khóa ORM(Object Relational Mapping). Khi mà thời đại của các framework ứng với các ngôn ngữ đang lên ngôi một cách mạnh

0 0 44