Giới thiệu về SOLID Design Principles
Nguồn: https://dotnettutorials.net/course/solid-design-principles/
Series: SOLID Design Principles trong C#
- Single Responsibility Principle (SRP) trong C#:
- Open-Closed Principle (OCP) trong C#:
- Liskov Substitution Principle (LSP) trong C#:
Tại sao chúng ta cần học các nguyên lý thiết kế SOLID?
Là lập trình viên, khi bắt đầu phát triển ứng dụng, chúng ta thường dựa vào kinh nghiệm và kiến thức hiện có. Tuy nhiên, theo thời gian, ứng dụng có thể phát sinh lỗi.
Mỗi khi có yêu cầu thay đổi hoặc bổ sung tính năng mới, chúng ta thường phải thay đổi thiết kế hiện tại. Và sau một thời gian, ngay cả những thay đổi đơn giản cũng trở nên rất tốn công sức, vì có thể đòi hỏi kiến thức tổng thể về toàn bộ hệ thống.
Nhưng chúng ta không thể đổ lỗi cho các yêu cầu thay đổi hay bổ sung tính năng, vì đó là một phần tất yếu trong quá trình phát triển phần mềm.
- Chúng ta không thể ngăn chặn những thay đổi đó,
- Và cũng không thể từ chối chúng.
Vậy vấn đề nằm ở đâu?
→ Rõ ràng là nằm ở thiết kế của ứng dụng.
Nguyên nhân chính dẫn đến sự thất bại của nhiều ứng dụng phần mềm là gì?
-
Nhồi nhét quá nhiều chức năng vào một lớp
(Nói đơn giản là: ta đưa quá nhiều chức năng vào trong một lớp, dù những chức năng đó không liên quan gì đến nhiệm vụ chính của lớp đó.)
👉 Điều này vi phạm nguyên lý SRP – một lớp chỉ nên đảm nhiệm một trách nhiệm duy nhất.
-
Tạo sự phụ thuộc chặt chẽ (tight coupling) giữa các thành phần phần mềm
(Tức là các lớp phụ thuộc lẫn nhau quá nhiều. Khi bạn thay đổi một lớp, các lớp khác bị ảnh hưởng theo.)
👉 Điều này khiến việc bảo trì, mở rộng và kiểm thử phần mềm trở nên khó khăn, dễ sinh lỗi.
Những vấn đề này xuất phát từ thiết kế sai và có thể được giải quyết hiệu quả bằng cách áp dụng các nguyên lý thiết kế SOLID, giúp phần mềm:
- Dễ hiểu hơn
- Linh hoạt hơn
- Dễ mở rộng và bảo trì hơn
Làm thế nào để khắc phục các vấn đề dẫn đến phát triển ứng dụng thất bại?
-
Sử dụng kiến trúc phần mềm phù hợp với yêu cầu của dự án
(Ví dụ: MVC, Layered Architecture, 3-tier Architecture, MVP, v.v.)
→ Mỗi loại kiến trúc có ưu điểm riêng và phù hợp với những loại dự án nhất định.
-
Tuân thủ các nguyên lý thiết kế phần mềm
Chẳng hạn như:
- SOLID Principles,
- ONION Architecture Principles, v.v.
→ Những nguyên lý này giúp chúng ta thiết kế phần mềm chặt chẽ, dễ mở rộng và dễ bảo trì.
-
Lựa chọn đúng các mẫu thiết kế phần mềm (Design Patterns) theo nhu cầu của dự án
Một số mẫu thiết kế thường dùng gồm:
- Creational Design Patterns (Mẫu tạo đối tượng – ví dụ: Singleton, Factory)
- Structural Design Patterns (Mẫu cấu trúc – ví dụ: Adapter, Decorator)
- Behavioral Design Patterns (Mẫu hành vi – ví dụ: Observer, Strategy)
- Dependency Injection Pattern
- Repository Pattern
→ Các design pattern này giúp chúng ta giải quyết những vấn đề phổ biến trong lập trình một cách hiệu quả và có tổ chức.
👉 Tóm lại, để phát triển một ứng dụng thành công, chúng ta không chỉ cần biết viết code, mà còn cần:
- Chọn đúng kiến trúc
- Áp dụng các nguyên lý thiết kế đúng cách
- Sử dụng mẫu thiết kế phù hợp với từng trường hợp
SOLID là gì? Các Nguyên lý Thiết kế SOLID là gì?
Các nguyên lý thiết kế SOLID được sử dụng để giải quyết hầu hết các vấn đề thiết kế phần mềm mà lập trình viên chúng ta thường gặp trong quá trình lập trình hằng ngày.
Đây là những nguyên lý đã được kiểm chứng và chứng minh hiệu quả, giúp thiết kế phần mềm trở nên:
- Dễ hiểu hơn,
- Linh hoạt hơn,
- Dễ bảo trì hơn.
👉 Khi áp dụng các nguyên lý này trong quá trình thiết kế ứng dụng, chúng ta có thể phát triển các phần mềm chất lượng cao hơn, dễ mở rộng và bảo trì lâu dài.
SOLID là viết tắt của 5 nguyên lý thiết kế sau:
🟢 S – Single Responsibility Principle (SRP)
Nguyên lý trách nhiệm đơn lẻ
➡ Mỗi lớp hoặc module chỉ nên chịu trách nhiệm về một việc duy nhất, tức là chỉ có một lý do để thay đổi.
Ví dụ: Một lớp
InvoicePrinter
chỉ nên xử lý việc in hóa đơn, không nên kiêm cả việc lưu hóa đơn vào database.
🟡 O – Open/Closed Principle (OCP)
Nguyên lý mở rộng – đóng
➡ Mỗi thực thể phần mềm (class, module, function...) nên mở cho việc mở rộng, nhưng đóng với việc chỉnh sửa.
Nói cách khác, ta nên thêm chức năng mới thông qua kế thừa hoặc extension thay vì chỉnh sửa code cũ.
🔵 L – Liskov Substitution Principle (LSP)
Nguyên lý thay thế Liskov
➡ Đối tượng của lớp con phải có khả năng thay thế đối tượng của lớp cha mà không làm sai hành vi của chương trình.
Ví dụ: Nếu
class Bird
có phương thứcFly()
, thì lớp conPenguin
không nên kế thừa nếu nó không bay được.
🟣 I – Interface Segregation Principle (ISP)
Nguyên lý phân tách giao diện
➡ Client không nên bị ép buộc phải triển khai những phương thức mà nó không cần.
Nên chia nhỏ interface theo từng nhóm chức năng chuyên biệt.
Ví dụ: Thay vì có một interface
IMachine
vớiPrint()
,Scan()
,Fax()
, nên chia thànhIPrinter
,IScanner
,IFax
.❌ Sai: Vi phạm ISP
public interface IMachine { void Print(); void Scan(); void Fax(); } public class OldPrinter : IMachine { public void Print() { Console.WriteLine("Printing..."); } public void Scan() { throw new NotImplementedException(); // Không có chức năng scan } public void Fax() { throw new NotImplementedException(); // Không có chức năng fax } }
Vấn đề:
OldPrinter
bị ép buộc phải implement những phương thức mà nó không cần.- Rõ ràng máy in cũ không có chức năng
Scan
hayFax
, nhưng vẫn phải khai báo chúng tronginterface
→ gây khó chịu và rối loạn thiết kế.✅ Đúng: Tuân thủ ISP
public interface IPrinter { void Print(); } public interface IScanner { void Scan(); } public interface IFax { void Fax(); } public class OldPrinter : IPrinter { public void Print() { Console.WriteLine("Printing..."); } } public class AllInOnePrinter : IPrinter, IScanner, IFax { public void Print() { Console.WriteLine("Printing..."); } public void Scan() { Console.WriteLine("Scanning..."); } public void Fax() { Console.WriteLine("Faxing..."); } }
Lợi ích:
- Mỗi class chỉ implement những interface mà nó thực sự cần.
- Thiết kế trở nên rõ ràng, dễ hiểu, và dễ mở rộng.
🔴 D – Dependency Inversion Principle (DIP)
Nguyên lý đảo ngược sự phụ thuộc
➡ Các module cấp cao không nên phụ thuộc trực tiếp vào module cấp thấp – cả hai nên phụ thuộc vào abstraction (trừu tượng).
Abstraction không nên phụ thuộc vào chi tiết, ngược lại: chi tiết nên phụ thuộc vào abstraction.
Ví dụ: Lớp
OrderService
không nên khởi tạo trực tiếpSqlDatabase
, mà nên làm việc thông quaIDatabase
interface.❌ Sai: Vi phạm DIP
public class SqlDatabase { public void SaveOrder(string orderData) { Console.WriteLine("Order saved to SQL Database"); } } public class OrderService { private SqlDatabase db = new SqlDatabase(); // phụ thuộc trực tiếp vào chi tiết SqlDatabase public void PlaceOrder(string orderData) { db.SaveOrder(orderData); } }
Vấn đề:
OrderService
phụ thuộc trực tiếp vào lớp cụ thểSqlDatabase
, nên:Nếu ta muốn đổi sang MongoDB hay file, phải chỉnh sửa lại code của
OrderService
→ phá vỡ OCP và DIP.✅ Đúng: Tuân thủ DIP
// Interface – Abstraction public interface IDatabase { void SaveOrder( string orderData ); } // Concrete implementation 1: SQL Database public class SqlDatabase : IDatabase { public void SaveOrder( string orderData ) { Console.WriteLine($"[SQL] Order saved to SQL Database: {orderData}"); } } // Concrete implementation 2: MongoDB public class MongoDatabase : IDatabase { public void SaveOrder( string orderData ) { Console.WriteLine($"[MongoDB] Order saved to MongoDB: {orderData}"); } } public class OrderService { private IDatabase db; // Constructor injection public OrderService( IDatabase db ) { this.db = db; } public void PlaceOrder( string orderData ) { Console.WriteLine("Placing order..."); db.SaveOrder(orderData); } }
internal class Program { static void Main(string[] args) { IDatabase db = new SqlDatabase(); IDatabase db_2 = new MongoDatabase(); OrderService orderService = new OrderService(db); orderService.PlaceOrder("Order #001"); OrderService orderService_2 = new OrderService(db_2); orderService_2.PlaceOrder("Order #002"); Console.ReadKey(); } }
Lợi ích:
OrderService
chỉ làm việc với abstractionIDatabase
, không quan tâm đến chi tiết bên dưới.- Ta có thể dễ dàng thay đổi hoặc thêm loại database khác (ví dụ MongoDB) mà không cần sửa
OrderService
.- Dễ dàng chuyển đổi qua lại giữa các database bằng dependency injection.
- Có thể mock
IDatabase
khi test (rất tiện cho Unit Test).
Lợi ích khi áp dụng SOLID:
- Code modular hơn (tách biệt rõ ràng),
- Dễ đọc hiểu và dễ thay đổi,
- Hạn chế phát sinh lỗi khi mở rộng hệ thống,
- Tăng khả năng tái sử dụng và kiểm thử tự động (Unit Test).
All rights reserved