Nguồn gốc SOLID
- Được hình thành từ cuối thập niên 1980, qua các tranh luận trên USENET (tiền thân của diễn đàn – forum).
- Được hoàn thiện vào đầu những năm 2000.
- Từ “SOLID” là gợi ý của Michael Feathers, khi phát hiện các chữ cái đầu ghép lại thành từ này.
Mục tiêu của các nguyên lý thiết kế là tạo ra các cấu trúc phần mềm cấp trung bình:
- Chịu đựng thay đổi,
- Dễ hiểu, và
- Là cơ sở của các component có thể được dùng trong nhiều hệ thống phần mềm.
SOLID
🔠 Tóm tắt 5 nguyên lý SOLID
Viết tắt | Tên đầy đủ | Ý chính |
---|---|---|
SRP | Single Responsibility Principle | Một module chỉ nên có một lý do để thay đổi |
OCP | Open-Closed Principle | Mở rộng qua code mới, không sửa đổi code cũ |
LSP | Liskov Substitution Principle | Thay thế object cùng kiểu mà không làm sai hành vi |
ISP | Interface Segregation Principle | Không phụ thuộc vào giao diện không dùng đến |
DIP | Dependency Inversion Principle | Code cấp cao không phụ thuộc vào chi tiết, mà ngược lại |
Single Responsibility – SRP – Một module chỉ nên có một lý do để thay đổi
❗ Hiểu sai phổ biến: “Mỗi module/class chỉ nên làm một việc” → đây là nguyên lý áp dụng cho hàm, không phải cho SRP trong SOLID.
✅ Định nghĩa đúng của SRP:
A module should have one, and only one, reason to change.
Định nghĩa mở rộng:
A module should be responsible to one, and only one, actor.
“Một mô-đun phải chịu trách nhiệm cho một, và chỉ một tác nhân.”
“Tác nhân” đề cập đến một nhóm một hoặc nhiều người yêu cầu thay đổi đó.
“Một mô-đun” hiểu là một tập hợp các chức năng và cấu trúc dữ liệu, trước đây module là một file, nhưng dần các hệ thống lưu trữ không lưu source code dưới dạng file nữa.
Tách source code cho các tác nhân khác nhau là cần thiết.
🧩 Ví dụ: Employee class vi phạm SRP
class Employee { double calculatePay(); // CFO (accounting) void reportHours(); // COO (HR) void save(); // CTO (DBA) }
❌ Vấn đề 1: Accidental Duplication
- Các chức năng cùng chia sẻ hàm regularHours()
int regularHours() { ... }
- Khi team CFO thay đổi
regularHours()
để tính lương chính xác hơn → gây lỗi cho báo cáo HR của COO vì cùng dùng chung logic nhưng mục đích khác nhau.
❌ Vấn đề 2: Conflict/Merge
- Hai team khác nhau sửa cùng một file
Employee.cs
→ dễ phát sinh merge conflict - Mỗi lần merge là một rủi ro mất ổn định hệ thống
✅ Giải pháp: Tách biệt theo trách nhiệm
📦 Giải pháp 1: Tách hàm khỏi dữ liệu
class EmployeeData { ... } // dữ liệu thuần class PayCalculator { double calculatePay(EmployeeData data) } class HourReporter { void reportHours(EmployeeData data) } class EmployeeSaver { void save(EmployeeData data) }
→ 3 class không biết đến nhau, giảm rủi ro ảnh hưởng lẫn nhau
🎭 Giải pháp 2: Dùng Facade pattern
class EmployeeFacade { PayCalculator calc; HourReporter hr; EmployeeSaver saver; double calculatePay() => calc.calculatePay(...); ... }
→ Tách nhỏ để maintain dễ dàng, đóng gói lại để sử dụng đơn giản
🎯 Giải pháp 3: Giữ hàm chính trong Employee
class Employee { double calculatePay() { ... } HourReporter hr; EmployeeSaver saver; }
→ Vẫn đảm bảo tách biệt logic khác, nhưng logic chính được đặt gần với dữ liệu hơn
💡 Lưu ý:
- Mỗi class không chỉ có một method, mà có thể có cả nhóm method phục vụ chung một actor.
- Các method phụ nên là private – giữ kín bên trong scope của class.
Open-Closed – OCP – Mở rộng qua code mới, không sửa đổi code cũ
📖 Định nghĩa gốc (Bertrand Meyer, 1988)
“A software artifact should be open for extension but closed for modification.”
“Cấu trúc phần mềm nên được mở để mở rộng nhưng bị đóng để sửa đổi.”
✅ Là nền tảng kiến trúc phần mềm bền vững, đặc biệt ở cấp độ component và hệ thống.
Việc phụ thuộc bắc cầu là vi phạm nguyên tắc chung.
Các bước để phân tích yêu cầu là:
- Đầu tiên, hãy phân biệt trách nhiệm của từng bước.
- Tiếp theo, tổ chức các phần phụ thuộc để đảm bảo các thay đổi đối với một trong các trách nhiệm đó không gây ra các thay đổi trong các quyền còn lại.
🧪 Ví dụ cách tư duy: In báo cáo từ web sang máy in đen trắng
📋 Yêu cầu ban đầu:
- Web page hiển thị dữ liệu tài chính, cuộn được, số âm tô màu đỏ.
🧾 Yêu cầu mới:
- Xuất báo cáo để in đen trắng
- Số âm đặt trong dấu ngoặc ()
- Có đầu trang, chân trang, phân trang, cột tiêu đề…
🔍 Câu hỏi kiến trúc:
Có cần sửa code cũ không? Bao nhiêu?
✅ Hệ thống tốt sẽ không cần sửa, chỉ cần mở rộng!
🧱 Giải pháp thiết kế theo OCP
🧩 Bước 1: Áp dụng SRP để tách 2 trách nhiệm
1. Phân tích dữ liệu tài chính
2. Trình bày (presentation) ra Web và Máy in
📐 Bước 2: Tổ chức kiến trúc theo OCP
📊 Hệ thống được chia thành các component:
Component | Vai trò chính |
---|---|
Interactor | Business Rules – trung tâm (cần được bảo vệ) |
Controller | Điều phối dữ liệu, liên kết UI |
Presenter | Chuyển dữ liệu thành dạng phù hợp cho View |
View | Hiển thị (Web, Print) |
Database | Lưu trữ dữ liệu |
📌 Tất cả các dependency là 1 chiều – theo hướng từ ngoài vào trong để bảo vệ lõi nghiệp vụ.
📌 Các interface như FinancialDataGateway
, FinancialReportPresenter
… dùng để đảo chiều phụ thuộc (áp dụng DIP).
📈 Cấu trúc phụ thuộc
- 🎯 Các component cấp cao → được bảo vệ khỏi thay đổi ở component cấp thấp
- 📶 Cấp bậc từ cao đến thấp:
- Interactor (business rules)
- Controller
- Presenter
- View
🔐 Về Information Hiding
- Interface
FinancialReportRequester
giúp bảo vệ Controller khỏi biết quá nhiều về Interactor. - Tránh transitive dependency – một trong những nguyên nhân gây khó mở rộng về sau.
Liskov Substitution – LSP – Thay thế object cùng kiểu mà không làm sai hành vi
📖 Định nghĩa gốc (Barbara Liskov, 1988)
“Nếu trong mọi ngữ cảnh sử dụng kiểu T, có thể thay thế đối tượng o2 thuộc T bằng đối tượng o1 thuộc S mà không làm thay đổi hành vi của chương trình, thì S là một subtype hợp lệ của T.”
✳️ Nói cách khác: “Hình vuông không phải là một kiểu con của Hình chữ nhật”.
“Một trong những lỗi vi phạm đơn giản về qui tắc thay thế có thể khiến kiến trúc của hệ thống bị vấn đục bởi một lượng đáng kể các chỉnh sửa.”
Nguyên tắc Liskov phát biểu: Nếu cứ mỗi đối tượng o1 của tập S có một o2 đối tượng của tập T sao cho tất cả chương trình P được xác định theo T. P không thay đổi khi o1 được thế bằng o2 ta gọi S sẽ là tập con của T.
⚙️ Mục tiêu của LSP
- Tạo ra mối quan hệ kế thừa hoặc interface đúng nghĩa.
- Tránh những thay đổi hoặc hành vi không mong muốn khi sử dụng polymorphism.
- Không vi phạm kỳ vọng của người dùng hệ thống (users of the type).
🧪 Ví dụ cổ điển: Square/Rectangle Problem
Rectangle r = new Square(); r.setWidth(5); r.setHeight(2); assert(r.area() == 10); // ❌ Assertion fail
- Vì Square ràng buộc
width == height
, nên thao tácsetWidth(5)
sau đósetHeight(2)
không hợp lệ với kỳ vọngRectangle
.
🧨 ➜ Vi phạm LSP vì Square không thể thay thế Rectangle một cách an toàn mà giữ nguyên hành vi mong đợi.
🧱 Ứng dụng LSP vào kiến trúc hệ thống
LSP không chỉ áp dụng cho kế thừa, mà còn mở rộng đến interface, REST API, và toàn bộ hệ thống.
🧩 Ví dụ thực tế: Hệ thống đặt taxi (REST interface)
- Giả định hệ thống gọi taxi qua URI chuẩn hóa:
/company/driver/Bob
/pickupAddress/24 Maple St.
/pickupTime/153
/destination/ORD
- Nhưng công ty Acme lại dùng
dest
thay vìdestination
.
→ Kết quả: hệ thống phải thêm if
kiểm tra đặc biệt:
if (driver.getDispatchUri().startsWith("acme.com")) ...
❌ Điều này:
- Làm nhiễm độc kiến trúc với xử lý đặc biệt
- Mở ra rủi ro bảo mật, lỗi logic, và vi phạm nguyên lý Information Hiding
✅ Giải pháp đúng:
- Trích thông tin cấu trúc từ cấu hình:
"Acme.com": "/pickupAddress/%s/pickupTime/%s/dest/%s"
"*.*": "/pickupAddress/%s/pickupTime/%s/destination/%s"
→ Cho thấy rằng: vi phạm LSP ở cấp API nhỏ cũng dẫn đến giải pháp phức tạp hóa toàn hệ thống
🔄 Mở rộng LSP trong thực tế
Áp dụng được với:
- Interface và các class cài đặt (Java, C#)
- Các class dùng duck typing (Ruby, Python)
- Các RESTful API
- Các plugin và module trong hệ thống lớn
✅ Mọi nơi có sự thay thế (substitution) → đều cần kiểm tra LSP.
Interface Segregation – ISP – Nguyên tắc Phân tách giao tiếp
“Phụ thuộc vào thứ gì đó đang mang một xe hành lý mà bạn không cần có thể gây ra cho bạn những rắc rối mà bạn không ngờ tới “.
Nguyên tắc phân tách giao tiếp (ISP) có thể được coi là một vấn đề ngôn ngữ hơn là một vấn đề kiến trúc.
Trên thực tế, việc áp dụng ISP không chỉ trả lời trên loại ngôn ngữ, ta có thể tránh những lần deploy không cần thiết do phụ thuộc code.
Giả sử rằng User1 chỉ sử dụng OPS.op1, User2 chỉ sử dụng OPS.op2 và User3 chỉ sử dụng OPS.op3. Ta nên sử dụng các giao diện: IUser1, IUser2, IUser3. Bằng cách này, User1, User2, User3 không quan tâm khi nào có sự thay đổi đối với OPS.
📊 Ví dụ
❌ Tình huống ban đầu:
interface OPS { void op1(); // dùng bởi User1 void op2(); // dùng bởi User2 void op3(); // dùng bởi User3 }
➡️ Kết quả:
Dù User1 chỉ cần op1, nhưng vẫn bị buộc phụ thuộc vào op2
, op3
→ Nếu op2()
thay đổi, User1 phải recompile và redeploy, dù không dùng gì liên quan.
✅ Giải pháp: Tách interface theo nhu cầu
interface U1Ops { void op1(); } interface U2Ops { void op2(); } interface U3Ops { void op3(); }
→ Giờ đây User1 chỉ phụ thuộc vào U1Ops
, không bị ảnh hưởng nếu op2
hoặc op3
đổi.
🧩 Phụ thuộc vào ngôn ngữ lập trình?
- Java, C# (statically typed):
- Các interface phải được import rõ ràng → tạo ra dependency ở cấp độ mã nguồn
- ➕ Dễ bắt lỗi sớm – ➖ Nhưng dễ dính recompile không cần thiết
- Ruby, Python (dynamically typed):
- Không cần khai báo cứng – kiểm tra interface lúc runtime
- → Không tạo dependency cứng → hệ thống mềm dẻo hơn, ít ràng buộc
👉 Có thể hiểu nhầm rằng ISP chỉ là vấn đề ngôn ngữ.
🏛️ Nhưng thực chất: ISP là vấn đề kiến trúc
“It is harmful to depend on modules that contain more than you need.”
🧱 Ví dụ kiến trúc:
- Hệ thống S muốn dùng framework F
- Nhưng F lại phụ thuộc cứng vào database D
- ➤ Nếu D thay đổi (dù S không dùng gì trong đó) → có thể gây lỗi toàn hệ thống
→ Vi phạm ISP ở cấp component → gây:
- Tái deploy không cần thiết
- Rủi ro lỗi dây chuyền
- Tăng coupling, giảm độ bền vững
🔁 Kết nối sang nguyên lý cao hơn
- Tư tưởng ISP sẽ được mở rộng và nhấn mạnh hơn ở chương sau:
🔗 Common Reuse Principle (Ch. 13 – Component Cohesion)
Dependecy Inversion – DIP – Code high-level không phụ thuộc vào chi tiết, mà ngược lại
“Một hệ thống linh hoạt là hệ thống trong đó các phụ thuộc mã nguồn chỉ gắn với các phần trừu tượng, không phải các cài đặt cụ thể”
Ranh giới kiến trúc tách biệt sự trừu tượng khỏi cài đặt. Luồng điều khiển hướng về phía trừu tượng trong khi các phần code phụ thuộc nghịch đảo với nó. Ta gọi nó là Depenecy Inversion.
📌 Ta nên viết code phụ thuộc vào interface (abstraction) thay vì class cụ thể (implementation).
✅ Điều này giúp hệ thống dễ thay đổi, test, mở rộng, và bảo trì.
🎯 Mục tiêu của DIP
- Tách logic nghiệp vụ (high level) khỏi chi tiết triển khai (low level).
- Giảm ảnh hưởng khi thay đổi class cụ thể.
- Áp dụng ở mọi cấp độ: class, module, service, component…
🧱 Quy tắc thực hành
⚠️ Tránh | ✅ Nên làm |
---|---|
Phụ thuộc trực tiếp vào class cụ thể | Phụ thuộc vào interface hoặc abstract class |
Kế thừa từ class dễ thay đổi (volatile) | Kế thừa từ abstraction |
Override hàm cụ thể dễ thay đổi | Đưa thành abstract rồi cài lại riêng |
Mention tên cụ thể trong code (ex: MySqlDb) | Dùng abstract interface (ex: IDatabase ) |
🧩 Tại sao phải cẩn trọng?
- Ví dụ:
String
trong C# là class cụ thể, nhưng ổn định → có thể chấp nhận. - Ngược lại, những class ta đang phát triển thường dễ thay đổi (volatile) → không nên phụ thuộc trực tiếp.
🧰 Giải pháp: Abstract Factory Pattern
Để tạo object cụ thể mà không vi phạm DIP, ta dùng Abstract Factory:
interface Service { void execute(); } interface ServiceFactory { Service makeSvc(); } class ConcreteImpl implements Service { ... } class ServiceFactoryImpl implements ServiceFactory { public Service makeSvc() { return new ConcreteImpl(); } }
Application
phụ thuộc vàoServiceFactory
, không biết gì vềConcreteImpl
main()
là nơi cấu hình cụ thể:
ServiceFactory factory = new ServiceFactoryImpl();
🔄 Dependency Inversion = Nghịch chiều điều khiển
- ⚙️ Source code dependency: từ
Application
→ServiceFactory
- 🔁 Control flow (dòng điều khiển): từ
ServiceFactoryImpl
→Application
👉 Hai chiều đi ngược nhau, gọi là Dependency Inversion
📐 Được mô tả bằng đường cong kiến trúc (curved line)

🧱 Concrete Component và “main”
- Vi phạm DIP luôn tồn tại ở đâu đó → gom lại vào một nơi duy nhất (thường là
main
) main
chịu trách nhiệm:- Khởi tạo Factory
- Tiêm dependencies cụ thể cho toàn hệ thống
Reference:
- Book: Clean Architecture (Robert C. Martin)
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.