01 Ba nguyên lý còn lại của SOLID
SRP và OCP (chủ đề 03) lo việc chia trách nhiệm và mở rộng không sửa. Ba nguyên lý còn lại bàn về các phần ghép lại với nhau ra sao — và đặc biệt là phụ thuộc giữa chúng nên chạy theo hướng nào.
LSP · Thay thế
Các phần thay thế cho nhau (interchangeable parts) phải tuân thủ một hợp đồng (contract) chung — subtype thế chỗ base mà chương trình vẫn đúng.
ISP · Tách giao diện
Đừng phụ thuộc vào thứ bạn không dùng. Interface béo kéo bạn dính vào những thay đổi chẳng liên quan.
DIP · Đảo phụ thuộc
Code high-level policy không phụ thuộc low-level detail; ngược lại, detail phụ thuộc policy qua các abstraction.
Sợi chỉ đỏ xuyên cả ba: vi phạm ở mức class (kế thừa sai, interface béo, gọi thẳng lớp cụ thể) rò rỉ lên kiến trúc — biến thành if/else đặc biệt, redeploy dây chuyền, và những ranh giới đặt sai chỗ.
02 LSP · Tính thay thế
Barbara Liskov (1988) định nghĩa subtype như sau: nếu mọi chương trình P viết theo kiểu T vẫn cư xử y hệt khi ta thay một đối tượng kiểu S vào chỗ của T, thì S là một subtype của T. Nói gọn: phần thay thế phải tôn trọng hợp đồng của phần bị thay.
Để dựng phần mềm từ các phần thay thế cho nhau, những phần đó phải tuân thủ một hợp đồng cho phép chúng thế chỗ lẫn nhau.
LSP · Clean ArchitectureVí dụ kinh điển: Square / Rectangle
Cho Square kế thừa Rectangle nghe rất "toán học", nhưng vi phạm LSP. Rectangle cho phép đổi width và height độc lập; Square thì buộc hai cạnh luôn đổi cùng nhau. Một User tưởng mình cầm Rectangle sẽ vỡ trận:
Rectangle r = makeRectangle(); // ...nhưng thực ra trả về một Square
r.setW(5);
r.setH(2);
assert(r.area() == 10); // ✗ Square sẽ cho 4 hoặc 25 → assertion thất bại
if trong User để dò xem Rectangle có thật là Square không. Một khi hành vi của User phụ thuộc vào kiểu cụ thể nó dùng, các kiểu đó không còn substitutable.LSP leo lên kiến trúc: vụ taxi dispatch
LSP không chỉ về inheritance. Nó áp cho mọi interface & implementation: interface kiểu Java, các lớp Ruby cùng method signature, hay nhiều dịch vụ cùng đáp một REST interface chung. Hãy nhìn điều gì xảy ra khi vi phạm:
Một hệ tổng hợp nhiều taxi dispatch service gửi lệnh điều xe qua REST, lấy URI từ hồ sơ tài xế. Mọi hãng phải tuân cùng một interface — cùng các trường pickupAddress, pickupTime, destination. Rồi lập trình viên của hãng Acme không đọc kỹ spec, viết tắt destination thành dest:
// Acme không substitutable → buộc phải có nhánh đặc biệt
if (driver.getDispatchUri().startsWith("acme.com")) {
// ...dựng URI bằng /dest/ thay vì /destination/
}
"acme" vào code mở cửa cho hàng loạt lỗi kỳ quái và lỗ hổng bảo mật. Acme mua thêm hãng khác? Lại thêm một if nữa cho từng thương hiệu.Vi phạm LSP
Square extends Rectangle: phá vỡ hợp đồng "đổi cạnh độc lập".- Acme bẻ cong REST interface chung (
dest≠destination). - Hệ quả: if/else đặc biệt rải vào kiến trúc, kiến trúc bị "ô nhiễm".
Tuân thủ LSP
- Subtype tôn trọng đúng contract của base.
- Cách ly khác biệt bằng module dựng lệnh dẫn bởi configuration database keyed theo URI.
- Mọi implementation thật sự thay thế được nhau ⟶ không nhánh đặc biệt.
Một vi phạm nhỏ về tính thay thế có thể khiến kiến trúc bị bơm thêm cả đống cơ chế thừa chỉ để dọn hậu quả. LSP nên được mở rộng tới tận mức kiến trúc.
03 ISP · Đừng phụ thuộc thứ bạn không dùng
Xét lớp OPS có ba thao tác op1, op2, op3. Ba user: User1 chỉ cần op1, User2 chỉ cần op2, User3 chỉ cần op3. Trong ngôn ngữ statically typed (Java), mã của User1 phụ thuộc cả OPS — nên nếu op2 đổi (thứ User1 chẳng quan tâm), User1 vẫn bị recompile & redeploy oan.
OPS thành U1Ops·U2Ops·U3Ops; OPS implement cả ba. Giờ User1 chỉ phụ thuộc U1Ops — đổi op2 không còn buộc User1 recompile/redeploy.ISP là vấn đề kiến trúc, không chỉ ngôn ngữ
Ngôn ngữ dynamically typed (Ruby, Python) suy ra phụ thuộc lúc runtime, không có source dependency cứng ⟶ dễ tưởng ISP chỉ là chuyện ngôn ngữ. Nhưng gốc rễ sâu hơn: phụ thuộc vào thứ mang theo "hành lý" (baggage) thừa luôn có hại.
Kiến trúc sư muốn dùng framework F trong hệ thống S, nhưng tác giả F trói nó vào database D: S → F → D. Nếu D có tính năng mà F không dùng, thì một thay đổi — hay một lỗi (failure) — trong phần thừa của D vẫn có thể buộc F và S redeploy, thậm chí gãy dây chuyền.
Gói cả ISP về một câu: "Đừng phụ thuộc vào những thứ bạn không cần." (Ở mức component, đây chính là Common Reuse Principle — bản tổng quát của ISP.)
04 DIP · Đảo ngược phụ thuộc
Hệ thống linh hoạt nhất là hệ mà mọi source code dependency chỉ trỏ tới abstraction, không tới concretion. Trong Java: import/use chỉ nên trỏ tới interface, abstract class — không gì cụ thể bị phụ thuộc cả.
Code hiện thực high-level policy không nên phụ thuộc vào code hiện thực low-level detail. Ngược lại mới đúng: detail phải phụ thuộc policy.
DIP · Clean ArchitectureChỉ tránh concretion volatile
Coi mọi thứ cụ thể là cấm kỵ thì phi thực tế: String trong Java cũng cụ thể nhưng cực kỳ stable — không cần trừu tượng hóa. Thứ cần tránh là các volatile concrete element: những module đang phát triển tích cực, thay đổi liên tục.
Stable abstractions: mỗi thay đổi của interface kéo theo thay đổi ở implementation, nhưng đổi implementation thường không đụng tới interface. Vậy nên interface luôn ít biến động hơn — kiến trúc sư giỏi cố thêm tính năng vào implementation mà không sửa interface.
Phụ thuộc concretion volatile
- Tham chiếu thẳng lớp cụ thể hay thay đổi.
- Kế thừa từ lớp cụ thể volatile (quan hệ source cứng nhất).
- Override hàm cụ thể — bạn không bỏ được phụ thuộc của nó, mà thừa kế luôn chúng.
Bốn quy tắc DIP
- Trỏ tới abstract interface, không tới lớp cụ thể volatile.
- Đừng kế thừa từ lớp cụ thể volatile.
- Thay vì override: làm hàm abstract rồi tạo nhiều implementation.
- Không bao giờ nhắc tên thứ vừa cụ thể vừa volatile.
Tạo đối tượng cụ thể bằng Abstract Factory
Khởi tạo một concrete object lại buộc có source dependency tới định nghĩa cụ thể của nó. Lối thoát: Abstract Factory. Application gọi makeSvc() trên interface ServiceFactory; phần ServiceFactoryImpl mới thật sự new ConcreteImpl rồi trả về dưới dạng Service trừu tượng — policy không bao giờ phải gọi tên lớp chi tiết.
Ranh giới kiến trúc & sự đảo chiều
Đây là ý cốt lõi nối sang phần Kiến trúc: interface trừu tượng vẽ nên một đường ranh giới kiến trúc. Mọi source dependency cắt qua ranh giới chỉ theo một hướng — về phía abstract. Nhưng flow of control lại đi ngược lại. Source dependency bị đảo so với luồng điều khiển — đó là lý do gọi là Dependency Inversion.
Không thể xóa sạch vi phạm DIP — luôn cần ai đó new ra lớp cụ thể. Hãy gom các vi phạm ấy vào vài module cấp thấp, thường là module main, tách khỏi phần còn lại của hệ thống.
05 Ba nguyên lý qua lăng kính RAG
Cùng hệ "Hỏi–đáp tài liệu": pipeline Python (FastAPI) lo chunk→embed→retrieve→generate; PHP (Laravel) chỉ chạm ở ranh giới service. Ba nguyên lý soi rõ ba quyết định khác nhau.
DIP · pipeline phụ thuộc abstraction
RAG Pipeline là high-level policy. Nó không được nhắc tên OpenAI hay FAISS (concretion volatile) — chỉ phụ thuộc interface VectorStore/LLM (stable abstraction). Một Abstract Factory dựng concrete; mọi vi phạm DIP gom về module main:
class VectorStore(Protocol): # stable abstraction
def search(self, q: list[float], k: int) -> list[Chunk]: ...
class LLM(Protocol):
def generate(self, prompt: str) -> str: ...
def answer(q, store: VectorStore, llm: LLM): # high-level policy, 0 concretion
ctx = store.search(embed(q), k=5)
return llm.generate(build(ctx, q))
# module `main` (vùng "bẩn") — nơi DUY NHẤT gọi tên lớp volatile
def make_store() -> VectorStore: return FaissStore(...) # Abstract Factory
def make_llm() -> LLM: return OpenAILLM(...)
VectorStore/LLM; flow of control chạy ra FaissStore/OpenAILLM — đảo chiều đúng tinh thần DIP.ISP · client chỉ cần search()
RAG Web client (PHP) chỉ truy hồi — đừng bắt nó phụ thuộc cả interface admin-index (upsert, reindex, drop). Tách Retriever khỏi IndexAdmin để đổi pipeline nạp dữ liệu không buộc client redeploy:
interface Retriever { // U1Ops của RAG: client chỉ cần đúng cái này
public function search(string $q, int $k): array;
}
// interface IndexAdmin { upsert(); reindex(); drop(); } ← client KHÔNG phụ thuộc
class AskController {
public function __construct(private Retriever $rag) {} // không dính "baggage" admin
public function ask(string $q): array { return $this->rag->search($q, 5); }
}
LSP · mọi VectorStore thay thế được nhau
RAG FaissStore và PgVectorStore phải thật sự substitutable: cùng nhận vector, trả top-k cùng ngữ nghĩa. Nếu một store lén đổi nghĩa tham số k (vd "ngưỡng điểm" thay vì "số kết quả"), pipeline buộc phải có nhánh if isinstance(...) — chính là vết rò Square/Rectangle.
Substitutable
store = FaissStore()hayPgVectorStore()—answer()không đổi một dòng.- Hợp đồng
search()giữ nguyên ngữ nghĩa.
Vi phạm LSP
- Một store đổi nghĩa
khoặc ném lỗi khác kiểu. - Pipeline phải thêm
iftheo loại store ⟶ kiến trúc nhiễm.
06 Ghi nhớ nhanh
LSP: subtype phải thế chỗ base mà chương trình vẫn đúng. Square/Rectangle vi phạm vì phá hợp đồng; vi phạm rò lên kiến trúc thành if/else đặc biệt.
ISP: đừng phụ thuộc vào thứ bạn không dùng. Tách interface béo; phụ thuộc module mang baggage thừa gây recompile/redeploy oan và lỗi dây chuyền.
DIP: phụ thuộc vào abstraction, tránh volatile concretion. Dùng Abstract Factory để dựng concrete; ưu tiên stable abstractions.
Đảo chiều: qua ranh giới kiến trúc, source dependency hướng về abstract còn flow of control đi ngược lại. Đây sẽ là Dependency Rule của mọi sơ đồ về sau.
Một câu chung cho ISP & DIP: chỉ phụ thuộc thứ ổn định & cần thiết; gom phần "bẩn" (vi phạm DIP không tránh được) vào module main.