Bóc tách nguyên lý Solid: Chìa khóa cho Code "sạch" và linh hoạt
Nguyên lý SOLID là tập hợp 5 nguyên lý thiết kế hướng đối tượng (OOP) giúp lập trình viên tạo ra phần mềm dễ bảo trì, linh hoạt và dễ mở rộng. Bài viết này sẽ giải thích chi tiết từng nguyên lý và cách áp dụng chúng để cải thiện thiết kế phần mềm.
Giới thiệu về Nguyên lý SOLID
Nguyên lý SOLID là tập hợp 5 nguyên lý thiết kế trong lập trình hướng đối tượng (OOP) giúp các nhà phát triển tạo ra phần mềm dễ bảo trì, linh hoạt và có khả năng mở rộng. Trọng tâm chính của những nguyên lý này là gì? Các nguyên lý này nhằm cải thiện thiết kế phần mềm và khuyến khích các phương pháp hay nhất. Vậy nên, nguyên lý SOLID thực chất là 5 nguyên lý được kết hợp trong từ "SOLID". Cùng tìm hiểu chi tiết nhé!
1- S => Single Responsibility Principle (SRP)
2- O => Open/Closed Principle (OCP)
3- L => Liskov Substitution Principle (LSP)
4- I => Interface Segregation Principle (ISP)
5- D => Dependency Inversion Principle (DIP)
Đây là 5 nguyên lý, bây giờ là lúc đi sâu vào từng nguyên lý.
1. Nguyên lý đơn nhiệm (Single Responsibility Principle - SRP)
- Định nghĩa: Một lớp chỉ nên có một lý do để thay đổi, nghĩa là nó chỉ nên có một chức năng.
- Lợi ích: Giảm độ phức tạp và cải thiện khả năng bảo trì mã bằng cách đảm bảo mỗi lớp xử lý một chức năng duy nhất.
"Nếu bạn không thấy nó hợp lý, hãy xem ví dụ tiếp theo, nếu không thì cứ bỏ qua"
//Violate SRP
public class UserManager {
public void AddUser(string Email , int Id) {
//Some code....
}
public void SendEmailToUser(int Id) {
//Some code....
}
public void SendReportToUser(string Email) {
//Some code....
}
}
Câu hỏi 1: Bạn nghĩ chúng ta nên làm gì để tuân theo nguyên lý Đơn Nhiệm (SR)?
Ví dụ dưới đây sử dụng một lớp UserManager để thực hiện nhiều hơn một chức năng (AddUser, SendEmailToUser, SendReportToUser). Để tuân theo SRP, mỗi lớp chỉ nên thực hiện một chức năng.
//Following the SRP
public class UserManager{
public void AddUser(string Email , int Id) {
//Some code..
}
}
public class EmailService{
public void SendEmail(int Id) {
//Some code..
}
}
public class ReportService{
public void SendReport(string Email) {
//Some code..
}
}
2. Nguyên lý Đóng/Mở (Open/Closed Principle - OCP)
- Định nghĩa: Các thực thể phần mềm (Lớp, Mô-đun, Hàm) nên có thể mở rộng, nhưng không thể sửa đổi. Tức là mở để mở rộng và đóng để sửa đổi.
- Lợi ích: Các chức năng mới có thể được thêm vào mà không làm thay đổi mã hiện có, giảm nguy cơ gây ra lỗi.
Hãy xem một ví dụ về vi phạm OCP.
public class PaymentService {
public void PaymentProccess(string paymentType) {
if(paymentType == "PayPal") {
//Some code....
}
if(paymentType == "CreditCard") {
//Some code....
}
if(paymentType == "BitCoin") {
//Some code....
}
}
}
Câu hỏi 2: Bạn nghĩ ví dụ cuối cùng vi phạm OCP như thế nào?! Bây giờ hãy tái cấu trúc theo OCP.
// #1: Create a interface
public interface IPaymentService {
void process();
}
// #2: Implement Payment Classes
public class PayPalPayment : IPaymentService {
public void Process() {
//Some code..
}
}
public class ICreditCardPayment : IPaymentService {
public void Process() {
//Some code..
}
}
Theo cách này, chúng ta đã làm cho mã của mình tuân theo OCP. Như thế nào?! Trước khi tái cấu trúc theo OCP, chúng ta phải thêm một câu lệnh if để kiểm tra paymentType. Sau khi tái cấu trúc theo OCP, chúng ta có thể dễ dàng chỉ cần thêm một lớp để xử lý paymentType mà không cần thay đổi mã cơ sở!
Public class BitcoinPayment : IPaymentService {
public void Process() {
//Some code..
}
}
3. Nguyên lý Thay thế Liskov (Liskov Substitution Principle - LSP)
- Định nghĩa: Các đối tượng của lớp cha phải có thể thay thế bằng các đối tượng của lớp con mà không ảnh hưởng đến tính đúng đắn của chương trình. Nói cách khác, giả sử bạn có một lớp cha gọi là [A] và lớp con gọi là [B], bạn sẽ có thể sử dụng [B] ở bất cứ đâu mà lớp [A] được sử dụng.
- Lợi ích: Khả năng bảo trì. Mã cơ sở sẽ không thay đổi hoặc bị ảnh hưởng khi bạn thêm mã hoặc chức năng bên ngoài.
//Superclass
public class Bird {
public virtual class Fly {
Console.WriteLine("This bird is flying");
}
}
//Subclass #1
public class sparrow : Bird {
public override void Fly() {
Console.WriteLine("The sparrow is flying");
}
}
//Subclass #2
public class Penguin : Bird {
//Surely penguins can't fly.. So we are passing incorrect info..
public override void Fly {
throw new NotImplementedException("Penguins can't fly!");
}
}
Điều này vi phạm LSP. Bởi vì nếu chúng ta sử dụng lớp penguin ở bất cứ đâu mà đối tượng Bird được mong đợi, chương trình sẽ hoạt động không như mong muốn. Việc gọi Fly trên lớp penguin sẽ ném ra một ngoại lệ... chim cánh cụt không thể bay!
Câu hỏi 3: Bạn có thể thử tìm ra cách tôi sẽ đạt được LSP không??
public abstract class Bird {
public abstract void Display();
}
//Create interface for flying behavior..
public interface IFlyable {
void Fly();
}
//Now we cant implement the subclass correctly...
public class sparrow : Bird, IFlyable {
public override void Display () {
Console.WriteLine("This is a sparrow!!");
}
public void Fly() {
Console.WriteLine("This sparrow is flying");
}
}
public class penguin : Bird {
public override void Display () {
Console.WriteLine("This is a pneguin!");
}
}
4. Nguyên lý Đảo ngược Sự phụ thuộc (Dependency Inversion Principle - DIP)
- Định nghĩa: Các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào trừu tượng (Interface/Abstract Classes).
- Lợi ích: Giảm sự ràng buộc chặt chẽ giữa các thành phần, làm cho hệ thống linh hoạt hơn và dễ dàng tái cấu trúc hoặc thiết lập lại hơn.
5. Nguyên tắc phân tách giao diện (ISP)
-
Định nghĩa: Nguyên tắc phân tách giao diện nêu rằng một lớp không nên bị ép buộc phải triển khai các giao diện mà nó không sử dụng. Thay vào đó, các giao diện lớn hơn nên được chia thành các giao diện nhỏ hơn, cụ thể hơn để các lớp triển khai chỉ cần quan tâm đến các phương thức có liên quan đến chúng
-
Lợi ích: Ngăn chặn các lớp triển khai các phương thức không liên quan. Làm cho mã trở nên mô-đun hơn và dễ hiểu hơn. Hỗ trợ Nguyên tắc trách nhiệm đơn (SRP) bằng cách tập trung giao diện vào các hành vi cụ thể.
Trường hợp vi phạm nguyên tắc:
public interface IAnimal {
void Eat();
void Fly();
void Swim();
}
public class Dog : IAnimal {
public void Eat() {
Console.WriteLine("Dog is eating.");
}
public void Fly() {
throw new NotImplementedException(); // Not relevant for a dog
}
public void Swim() {
Console.WriteLine("Dog is swimming.");
}
}
Ví dụ cuối cùng vi phạm Nguyên tắc phân tách giao diện (ISP) vì nó buộc lớp Dog phải triển khai các phương thức (Fly) không liên quan hoặc không cần thiết cho hành vi của nó. Điều này tạo ra một số vấn đề.
Chúng ta hãy thử tái cấu trúc theo nguyên tắc:
public interface IEater {
void Eat();
}
public interface ISwimmer {
void Swim();
}
public class Dog : IEater, ISwimmer {
public void Eat() {
Console.WriteLine("Dog is eating.");
}
public void Swim() {
Console.WriteLine("Dog is swimming.");
}
}
bằng cách chia giao diện IAnimal thành các giao diện nhỏ hơn, cụ thể về hành vi như IEater, IFlyer và ISwimmer, mỗi lớp chỉ có thể triển khai các giao diện có liên quan đến hành vi của nó. Điều này tránh được các vấn đề được đề cập ở trên và tuân thủ ISP, làm cho cơ sở mã sạch hơn, dễ bảo trì hơn và linh hoạt hơn
Như vậy bài viết này đã trình bày cho các bạn đầy đủ chi tiết về nguyên lý SOLID, hy vọng chúng sẽ giúp ích cho các bạn trong quá trình lập trình.
All rights reserved