01 Ba nguyên lý ghép nối component
Sau ba nguyên lý cohesion (lớp nào thuộc component nào), tới lượt ba nguyên lý coupling điều phối các mũi tên phụ thuộc giữa component. Khác với cohesion, ba nguyên lý này không thể thiết kế trước — chúng tiến hóa cùng hệ thống, là tấm bản đồ về tính dễ-build và dễ-bảo-trì.
ADP
Không cho phép chu trình trong đồ thị phụ thuộc. Đồ thị phải là một DAG (đồ thị có hướng, không chu trình).
SDP
Phụ thuộc theo hướng ổn định. Component dễ đổi không được bị component khó đổi phụ thuộc vào.
SAP
Càng ổn định thì càng phải trừu tượng. Ổn định mà cụ thể = cứng nhắc; bất ổn mà trừu tượng = vô dụng.
Đồ thị phụ thuộc component không mô tả chức năng của ứng dụng — nó là bản đồ build & maintenance. Vì thế nó không được vẽ từ đầu dự án (lúc đó chưa có gì để build), mà lớn lên cùng thiết kế logic.
02 ADP — Không cho phép chu trình
Không cho phép bất kỳ chu trình nào trong đồ thị phụ thuộc của các component.
The Acyclic Dependencies PrincipleBạn từng làm cả ngày, code chạy ngon, về nhà — sáng hôm sau quay lại thì hỏng hết? Vì có ai đó ở lại muộn hơn và sửa một component bạn đang phụ thuộc vào. Martin gọi đây là "the morning after syndrome" — và nó càng thảm khốc khi đội & dự án càng to.
Từ "weekly build" tới khủng hoảng tích hợp
Giải pháp cũ: cả tuần ai làm việc nấy, thứ Sáu mới tích hợp (weekly build). Nhưng khi dự án phình to, gánh nặng tích hợp tràn sang thứ Bảy, rồi lùi lên thứ Năm, rồi thành biweekly build… lịch build cứ phải kéo dài, làm tăng rủi ro và mất đi phản hồi nhanh.
Lời giải đúng: chia môi trường thành các releasable component, mỗi cái có release number riêng. Đội khác tự chọn thời điểm nâng cấp thay vì bị ảnh hưởng tức thì — tích hợp diễn ra theo từng bước nhỏ, không có "điểm hẹn" toàn đội.
Phát hiện & phá chu trình
Khi đồ thị xuất hiện chu trình (vd A → B → C → A), các component trong vòng thực chất dính thành một khối: phải build, test, release cùng nhau. Có hai cách phá vòng:
flowchart LR
subgraph BAD["✗ Có chu trình"]
direction LR
e1["Entities"]:::core --> au1["Authorizer"]:::out
au1 --> u1["User"]:::out
u1 -. "vòng ngược" .-> e1
end
subgraph FIX["✓ Phá bằng DIP (chèn interface)"]
direction LR
e2["Entities"]:::core --> pi["«interface»
Permissions"]:::core
au2["Authorizer"]:::out -. implements .-> pi
u2["User"]:::out --> e2
end
BAD --> FIX
classDef core fill:#14233b,stroke:#0b1626,color:#f4efe3;
classDef out fill:#e2edf3,stroke:#2f6d93,color:#1c1a14;
Permissions) vào Entities, cho Authorizer implement nó → đảo chiều mũi tên, phá vòng. Cách 2 — component mới: tách các lớp mà cả hai cùng phụ thuộc ra một component thứ ba để cả hai trỏ tới."The jitters": cấu trúc component rung và lớn lên theo yêu cầu đổi. Vì vậy phải liên tục giám sát chu trình; mỗi lần chu trình xuất hiện là một lần phải phá — đôi khi sinh ra component mới khiến đồ thị ngày càng phình.
03 SDP — Phụ thuộc theo hướng ổn định
Hãy phụ thuộc theo hướng của sự ổn định.
The Stable Dependencies Principle · Depend in the direction of stability"Ổn định" không phải tần suất thay đổi — mà là lượng công sức cần để tạo ra một thay đổi. Dựng đồng xu trên cạnh: nó không đổi nhưng chẳng ổn định, vì chỉ cần khẽ chạm là đổ. Một cái bàn thì ổn định vì rất khó lật.
Trong phần mềm: một component bị nhiều component khác phụ thuộc vào sẽ rất ổn định — nó có nhiều lý do để không đổi (đổi là phải hoà giải với tất cả những kẻ phụ thuộc). Trớ trêu thay, một module bạn thiết kế để dễ đổi có thể bị người khác "treo" một dependency lên và bỗng trở nên khó đổi.
Ổn định · I = 0
Nhiều Fan-in, không Fan-out. Bị phụ thuộc nhiều (có trách nhiệm), không phụ thuộc ai (độc lập) → khó đổi nhất.
Bất ổn · I = 1
Không Fan-in, nhiều Fan-out. Không ai phụ thuộc (vô trách nhiệm), lại phụ thuộc nhiều thứ → dễ đổi, dễ bị buộc phải đổi.
Đo Instability: I = Fan-out / (Fan-in + Fan-out)
| Ký hiệu | Ý nghĩa | Giá trị I |
|---|---|---|
| Fan-in | Số lớp ngoài component phụ thuộc vào lớp bên trong nó (incoming). | I cao ⟶ Fan-in thấp |
| Fan-out | Số lớp bên trong phụ thuộc ra lớp ngoài component (outgoing). | I cao ⟶ Fan-out cao |
| I = 0 | Fan-out = 0. Ổn định tối đa — như cái bàn. | ổn định nhất |
| I = 1 | Fan-in = 0. Bất ổn tối đa — như đồng xu dựng cạnh. | bất ổn nhất |
SDP yêu cầu: chỉ số I phải GIẢM dần theo hướng phụ thuộc. Component dễ đổi (I cao) nằm "trên cao" và trỏ xuống component ổn định (I thấp) "dưới thấp". Mọi mũi tên chỉ lên trên đều vi phạm SDP.
04 SAP — Ổn định thì phải trừu tượng
Một component nên trừu tượng đúng bằng mức nó ổn định.
The Stable Abstractions Principle · as abstract as it is stableHigh-level policy (chính sách nghiệp vụ, quyết định kiến trúc) không nên hay đổi → phải đặt vào component ổn định (I = 0). Nhưng nếu code chính sách nằm trong component cứng như đá thì làm sao mở rộng? Câu trả lời là OCP: dùng abstract class và interface để mở rộng được mà không cần sửa.
SAP nói: hai vị trí dưới đây đều ĐÚNG — chúng là hai đầu lành mạnh của "chuỗi chính". Vùng cấm là khi lệch khỏi cả hai (xem Main Sequence ở mục sau).
Ổn định ⟹ trừu tượng
- Component ổn định gồm interface & abstract class → vẫn mở rộng được dù khó sửa.
- Ổn định + trừu tượng = linh hoạt, không trói buộc kiến trúc.
Bất ổn ⟹ cụ thể
- Component bất ổn nên cụ thể (concrete) — vì dễ đổi nên cứ để code cụ thể bên trong.
- Hai vùng CẤM là ngược lại: ổn định + cụ thể (Zone of Pain) hoặc bất ổn + trừu tượng (Zone of Uselessness).
Đo Abstractness: A = Na / Nc
Nc
Tổng số lớp trong component.
Na
Số abstract class & interface trong component.
A = Na / Nc
A = 0: hoàn toàn cụ thể. A = 1: toàn abstraction.
SDP + SAP gộp lại chính là DIP ở cấp component: SDP nói "phụ thuộc chạy về phía ổn định", SAP nói "ổn định kéo theo trừu tượng" → phụ thuộc chạy về phía trừu tượng. Khác với DIP cấp lớp (trắng/đen), ở cấp component cho phép nửa-trừu-tượng, nửa-ổn-định.
05 Main Sequence & khoảng cách D
Vẽ A (trục tung) theo I (trục hoành), ta thấy hai vị trí "tốt": ổn định-trừu-tượng ở góc trên-trái (0, 1) và bất-ổn-cụ-thể ở góc dưới-phải (1, 0). Hai góc còn lại là vùng cấm (zones of exclusion).
(0,1)–(1,0) là Main Sequence: nơi component nên nằm.Distance: D = |A + I − 1|
Khoảng cách từ component tới Main Sequence: D = |A + I − 1|, phạm vi [0, 1]. D ≈ 0 nghĩa là nằm ngay trên đường lý tưởng; D gần 1 nghĩa là cần xem xét & tái cấu trúc. Có thể tính mean/variance của D toàn hệ thống, đặt "control limit" để soi các component bất thường.
Ngoại lệ vô hại: một utility cụ thể như String nằm sát Zone of Pain nhưng không biến động (nonvolatile) nên vô hại. Chỉ component hay đổi mà rơi vào vùng cấm mới thực sự "đau".
06 Ghi nhớ nhanh
ADP: đồ thị phụ thuộc phải là DAG — không chu trình. Phá vòng bằng DIP (chèn interface, đảo chiều) hoặc tách component mới. Giám sát liên tục vì cấu trúc luôn "jitter".
SDP: phụ thuộc theo hướng ổn định — I = Fan-out/(Fan-in+Fan-out) phải GIẢM dần theo hướng mũi tên. Đừng để component khó đổi phụ thuộc vào component dễ đổi.
SAP: càng ổn định càng phải trừu tượng (A = Na/Nc). High-level policy đặt vào component ổn định + abstraction (OCP) để mở rộng mà không sửa.
Main Sequence: né Zone of Pain (ổn định+cụ thể) và Zone of Uselessness (trừu tượng+bất ổn). Đo bằng D = |A+I−1|, càng gần 0 càng tốt.
Đồ thị component là bản đồ build, không phải sơ đồ chức năng. Nó tiến hóa cùng hệ thống — không vẽ trước, mà quản lý liên tục để né "the morning after syndrome".
RAG Soi qua hệ "Hỏi–đáp tài liệu"
Áp ba nguyên lý lên hệ RAG: Python (FastAPI) lo pipeline (chunk→embed→retrieve→generate); PHP (Laravel) chỉ ở ranh giới service. Ta tách thành các component: rag-core (chính sách, abstraction), ingestion, retriever, store, api.
SDP — rag-core ổn định, mọi thứ trỏ về nó
RAG rag-core chỉ chứa Protocol (interface) → Fan-out ≈ 0 → I ≈ 0. ingestion và api phụ thuộc vào nó, không bao giờ ngược lại — I giảm dần theo hướng phụ thuộc.
# rag-core/ports.py — ỔN ĐỊNH: chỉ abstraction, không Fan-out
class Retriever(Protocol):
def search(self, query: str, k: int) -> list[Chunk]: ...
# ingestion/pipeline.py — BẤT ỔN: phụ thuộc XUỐNG rag-core
from rag_core.ports import Retriever # mũi tên trỏ về phía ổn định ✓
def index(docs, retriever: Retriever): ... # rag-core KHÔNG biết tới ingestion
import đi từ component bất ổn (ingestion, api) về phía rag-core. Nếu rag-core lỡ import ngược lên ingestion → I của nó tăng, vi phạm SDP. ADP — phá chu trình retriever ⇄ store
RAG retriever gọi store để lấy vector, nhưng store lại gọi retriever để rerank → chu trình. Chèn interface vào rag-core để đảo chiều (DIP):
# rag-core/ports.py — interface chung, cả hai cùng phụ thuộc xuống
class Reranker(Protocol):
def rerank(self, chunks: list[Chunk]) -> list[Chunk]: ...
# store/vector_store.py — KHÔNG còn import retriever; nhận Reranker qua tham số
def query(self, vec, reranker: Reranker): # vòng ngược bị phá ✓
return reranker.rerank(self._knn(vec)) # đồ thị trở lại DAG
retriever → store → retriever (cycle). Sau: cả hai trỏ xuống Reranker trong rag-core. Cách 2 (tách component rerank riêng) cũng phá được vòng tương tự. SAP — rag-core ổn định nên chỉ chứa abstraction
RAG Vì rag-core ổn định nhất (I≈0), nó phải trừu tượng nhất (A≈1) — toàn Protocol, không logic cụ thể. PHP ở ranh giới chỉ gọi service qua HTTP, không chạm vào abstraction này.
// app/Services/RagGateway.php — component bất ổn: cụ thể, dễ đổi
class RagGateway {
public function ask(string $q): array {
// chỉ gọi HTTP sang FastAPI; không phụ thuộc rag-core
return Http::post(config('rag.url').'/answer', ['q' => $q])->json();
}
}
rag-core (Python): A≈1, I≈0 → nằm góc trên-trái Main Sequence. RagGateway (PHP): cụ thể, không ai phụ thuộc vào → I≈1, A≈0 → góc dưới-phải. Cả hai đều xa hai vùng cấm.