01 Policy & Level: sắp xếp theo khoảng cách I/O
Một hệ thống phần mềm là một tập hợp các tuyên bố policy. Một chương trình chỉ là bản mô tả chi tiết về policy biến input thành output.
Robert C. Martin · Chương 19Trong hệ thống thực, policy lớn ấy được chẻ thành nhiều policy con: tính quy tắc nghiệp vụ, định dạng báo cáo, xác thực dữ liệu… Nghệ thuật kiến trúc là tách các policy rồi nhóm lại theo cách chúng thay đổi:
Nhóm lại
Policy đổi cùng lý do, cùng thời điểm ⟶ cùng một level, đặt chung một component (đúng tinh thần SRP & CCP).
Tách ra
Policy đổi vì lý do khác / lúc khác ⟶ khác level, phải tách vào component khác nhau.
Định nghĩa Level = "khoảng cách tới input và output" (distance from the inputs and outputs). Càng xa I/O ⟶ level càng cao. Policy quản input/output là policy cấp thấp nhất. Policy cấp cao đổi ít hơn, vì lý do quan trọng hơn; policy cấp thấp đổi thường xuyên, khẩn cấp, nhưng vì lý do ít quan trọng hơn.
Data flow ≠ Dependency: phụ thuộc luôn trỏ LÊN cấp cao
Lấy ví dụ chương trình mã hoá (encryption): đọc ký tự ⟶ dịch qua bảng ⟶ ghi ký tự. readChar/writeChar chạm thẳng thiết bị I/O nên cấp thấp; Translate xa I/O nhất nên cấp cao nhất. Điều cốt lõi: luồng dữ liệu và hướng phụ thuộc mã nguồn không cùng chiều.
flowchart LR
subgraph DF["① Data flow · luồng dữ liệu"]
direction LR
R["readChar
(thấp · gần input)"]:::out --> T1["Translate
(cao · xa I/O)"]:::core --> W["writeChar
(thấp · gần output)"]:::out
end
subgraph SD["② Source dependency · phụ thuộc mã nguồn"]
direction LR
RC["ConsoleReader"]:::out -.->|trỏ LÊN| E["Encryption
(cấp cao)"]:::core
WC["ConsoleWriter"]:::out -.->|trỏ LÊN| E
end
DF --> SD
classDef core fill:#dce7f1,stroke:#2e5e8c,color:#14233b;
classDef out fill:#e2edf3,stroke:#2f6d93,color:#14233b;
Encryption cấp cao — qua interface CharReader/CharWriter. Component cấp thấp "cắm" (plugin) vào cấp cao; nhờ vậy thuật toán mã hoá tái dùng được với mọi thiết bị I/O. Decouple phụ thuộc khỏi data flow, gắn nó vào level.Nếu hàm cấp cao encrypt gọi thẳng readChar/writeChar ⟶ kiến trúc sai: policy mã hoá bị dính vào chi tiết thiết bị. IO device dễ đổi hơn thuật toán mã hoá nhiều — nên để cái dễ đổi (cấp thấp) phụ thuộc vào cái ổn định (cấp cao), không bao giờ ngược lại.
02 Entity: Critical Business Rules & Data
Đỉnh cao nhất của các policy là business rules. Nói chặt chẽ, business rule là quy tắc/thủ tục giúp doanh nghiệp kiếm tiền hoặc tiết kiệm tiền — và chúng vẫn tồn tại ngay cả khi không có hệ thống máy tính nào tự động hoá chúng.
Critical Business Rules
Quy tắc cốt yếu của chính doanh nghiệp. Ngân hàng tính lãi N% cho khoản vay là một critical rule — dù tính bằng máy tính hay bằng bàn tính abacus đều thế.
Critical Business Data
Dữ liệu mà các quy tắc trên cần để vận hành, cũng tồn tại độc lập với tự động hoá: số dư khoản vay, lãi suất, lịch thanh toán…
Entity = Critical Business Rules + Critical Business Data. Một object đóng gói tập quy tắc cốt lõi vận hành trên dữ liệu cốt lõi — ví dụ entity Loan giữ balance · interestRate · paymentSchedule và phơi ra các hàm tính lãi. Entity là pure business: không vẩn đục bởi database, UI, hay framework bên thứ ba, nên dùng được trong bất kỳ hệ thống nào.
RAG · Entity là quy tắc cốt lõi, không phụ thuộc framework
RAG Quy tắc "thế nào là một Chunk hợp lệ" (độ dài tối đa, không rỗng) là critical business rule của hệ RAG — đúng kể cả khi bạn chunk tài liệu bằng tay. Nó phải sống trong Entity, không dính FastAPI hay vector DB:
# Entity: Critical Business Rules + Data — pure business, KHÔNG biết web/DB
MAX_CHUNK = 800 # quy tắc cốt lõi: chunk vượt ngưỡng là không hợp lệ
class Chunk:
def __init__(self, text: str, doc_id: str):
if not text.strip(): # Critical Business Rule
raise ValueError("chunk rỗng")
if len(text) > MAX_CHUNK: # tồn tại kể cả khi chunk thủ công
raise ValueError("chunk quá dài")
self.text, self.doc_id = text, doc_id
import fastapi, không import qdrant. Chunk có thể phục vụ bất kỳ hệ thống nào — đúng tinh thần "Entity là pure business".03 Use Case: quy tắc đặc thù ứng dụng & điều phối
Nếu Entity là quy tắc của doanh nghiệp, thì Use Case là quy tắc của ứng dụng cụ thể. Use case mô tả cách một hệ thống tự động được dùng: định nghĩa input người dùng cấp, output trả về, và các bước xử lý để tạo ra output đó.
Use Case = application-specific business rules. Nó điều phối (orchestrate) luồng dữ liệu ra/vào Entity và "điều khiển vũ điệu của các Entity" — chỉ huy chúng dùng Critical Business Rules để đạt mục tiêu của use case. Tầng use case cô lập khỏi DB, UI, framework.
Use Case KHÔNG biết mình được phân phối kiểu gì
Use case không mô tả giao diện. Nhìn vào một use case, bạn không thể biết ứng dụng chạy trên web, thick client, console hay là một pure service — nó chỉ nói tới dữ liệu vào ra, mặc kệ cách dữ liệu đó được truyền đi.
| Khía cạnh | Entity | Use Case |
|---|---|---|
| Quy tắc gì | Critical Business Rules (của doanh nghiệp) | Application-specific rules (của ứng dụng) |
| Tồn tại không cần máy tính? | có — kể cả tính tay | không — mô tả cách hệ thống tự động được dùng |
| Khoảng cách tới I/O | Xa nhất ⟶ cấp cao | Gần I/O hơn ⟶ cấp thấp hơn |
| Tái dùng được ở nhiều app? | Có — generalization | Không — đặc thù một app |
RAG · Use Case điều phối retrieve → rank → generate
RAG AnswerQuestion là use case: nó không tự embed, không tự gọi LLM bằng tay — nó điều phối các bước và để Entity giữ quy tắc cốt lõi:
@dataclass # Request/Response model: cấu trúc thuần, KHÔNG kế thừa HttpRequest
class AskRequest: question: str
@dataclass
class AskResponse: answer: str; sources: list[str]
class AnswerQuestion: # application-specific use case
def execute(self, req: AskRequest) -> AskResponse:
chunks = self.retriever.search(req.question) # điều phối: retrieve
ranked = self.ranker.rank(chunks) # → rank
ans = self.generator.generate(ranked, req.question) # → generate
return AskResponse(answer=ans.text, sources=ans.cited)
AskRequest/AskResponse "không biết gì về web": không HTML, không SQL, không HttpRequest. Use case chỉ điều phối — quy tắc cốt lõi nằm trong Entity / generator policy.04 Use Case ⟶ Entity: hướng phụ thuộc một chiều
Đây là điểm quan trọng nhất của chương 20, và là hệ quả trực tiếp của định nghĩa level ở phần 1: khái niệm cấp cao không biết gì về khái niệm cấp thấp hơn.
Hai mặt dưới đây là cùng một quy tắc nhìn từ hai phía — cả hai đều là tính chất mong muốn, không có bên "đúng" bên "sai".
Use Case PHỤ THUỘC Entity
- Use case nhắc tên Entity, gọi hàm của Entity để điều phối.
- Use case đặc thù một app ⟶ gần I/O ⟶ cấp thấp hơn.
- Hướng phụ thuộc trỏ VÀO Entity (Dependency Inversion).
Entity KHÔNG biết Use Case
- Entity là generalization dùng được cho nhiều app ⟶ xa I/O ⟶ cấp cao nhất.
- Entity không hề nhắc tên use case nào.
- Đổi use case không được phép động tới Entity.
flowchart TB WEB["Web / Console / Service
(detail · cấp thấp nhất)"]:::out --> UC UC["Use Case · AnswerQuestion
application-specific · cấp thấp hơn"]:::mid UC -->|phụ thuộc · trỏ LÊN| ENT["Entity · Chunk / Loan
Critical Business Rules · CẤP CAO NHẤT"]:::core UC -.->|trả về| RR["Request / Response model
(cấu trúc thuần · không framework)"]:::out classDef core fill:#14233b,stroke:#2e5e8c,color:#ffffff; classDef mid fill:#dce7f1,stroke:#2e5e8c,color:#14233b; classDef out fill:#e2edf3,stroke:#2f6d93,color:#14233b;
HttpRequest/HttpResponse, không biết gì về web hay UI.Đừng để Request/Response model tham chiếu Entity. Tưởng tiện vì chúng "chung nhiều dữ liệu", nhưng mục đích hai loại object rất khác và sẽ đổi vì lý do khác nhau ⟶ buộc chúng dính nhau là vi phạm CCP & SRP, sinh ra "tramp data" và đầy điều kiện rối rắm.
RAG · level trong pipeline: embed gần I/O, AnswerQuestion xa I/O
RAG Xếp các thành phần RAG theo khoảng cách tới I/O: tokenizer/embed chạm thẳng dữ liệu thô ⟶ cấp thấp; AnswerQuestion điều phối ⟶ cấp cao hơn; quy tắc Chunk ⟶ cao nhất. Ranh giới ra ngoài để PHP gọi sang chỉ là một detail cấp thấp:
// PHP = detail cấp thấp nhất (gần I/O người dùng) — KHÔNG chứa business rule
// Phụ thuộc trỏ VÀO Python: PHP biết use case, use case không biết PHP/web
$res = Http::post('http://rag-svc/ask', ['question' => $q]);
return $res->json('answer'); // Request/Response model thuần băng qua ranh giới
// Bên trong rag-svc: AnswerQuestion (cấp cao) điều phối Chunk-entity (cao nhất)
AnswerQuestion và Chunk không hề hay biết, vì chúng ở cấp cao hơn và không phụ thuộc cách phân phối.05 Ghi nhớ nhanh
Level = khoảng cách tới I/O. Càng xa input/output ⟶ cấp càng cao. Tách & nhóm policy theo cách chúng thay đổi: cùng lý do/lúc ⟶ chung component.
Phụ thuộc trỏ LÊN cấp cao. Data flow ≠ dependency. Component cấp thấp "cắm" (plugin) vào cấp cao qua interface — như readChar/writeChar trỏ vào Encryption.
Entity = Critical Business Rules + Data — quy tắc kiếm/giữ tiền, tồn tại kể cả không có máy tính (ví dụ Loan với interest). Pure business, không dính DB/UI/framework.
Use Case = application-specific, điều phối "vũ điệu của Entity", định nghĩa input/output. Không mô tả UI — không biết mình chạy trên web hay console.
Use Case phụ thuộc Entity — KHÔNG ngược lại. Dữ liệu qua ranh giới đi bằng Request/Response model thuần (không HttpRequest); đừng cho model tham chiếu Entity.