Clean Architecture
12 Phần V · Chi tiết Chương 30–34

Chi tiết: DB · Web · Framework · Tổ chức

Details · Case Study · The Missing Chapter

Bốn chương cuối khép lại cuốn sách bằng một lời nhắc lạnh lùng: database, web, và framework đều chỉ là chi tiết (details) — những cơ chế IO ở rìa ngoài, không phải linh hồn của hệ thống. Một case study bán video minh họa toàn bộ lý thuyết thành hình; và "Chương còn thiếu" của Simon Brown cảnh báo rằng kiến trúc đẹp nhất vẫn sụp đổ nếu code không khớp với nó.

01 Tất cả đều là chi tiết

Cả Phần V của cuốn sách lặp đi lặp lại một thông điệp: những thứ developer hay coi là "trung tâm" — cơ sở dữ liệu, giao diện web, framework — thực ra chỉ là cơ chế (mechanisms) ở vòng ngoài cùng. Lõi nghiệp vụ (business rules) phải không hề biết chúng tồn tại. Quan hệ của database với kiến trúc, theo Martin, "giống như nắm cửa với kiến trúc của ngôi nhà".

Mục tiêu của kiến trúc sư là tạo ra một shape cho hệ thống sao cho policy là yếu tố cốt lõi còn các details trở nên không liên quan (irrelevant) tới policy. Nhờ vậy, quyết định về details có thể được trì hoãn và đẩy lùi (delay & defer).

flowchart LR
  subgraph OUT["Frameworks & Drivers — nơi mọi chi tiết trú ngụ"]
    DB["Database
(detail)"]:::det WEB["Web / GUI
(detail)"]:::det FW["Framework
(detail)"]:::det end OUT -->|"phụ thuộc trỏ VÀO TRONG"| CORE["Business Rules
Use Cases · Entities"]:::core classDef det fill:#f1dbe0,stroke:#9c3450,color:#14233b; classDef core fill:#14233b,stroke:#14233b,color:#f3ece0;
Theo Dependency Rule, mọi phụ thuộc mã nguồn trỏ vào trong. DB, Web, Framework nằm ở vòng ngoài — nơi chúng "gây hại được rất ít" và có thể thay thế khi lỗi thời mà không động tới lõi.

Vì sao tách bạch như vậy lại đáng giá? Vì nó cho phép bạn viết và kiểm thử business rules trước khi quyết định dùng Oracle, MySQL hay file phẳng — trước khi biết app sẽ chạy trên web, console hay thick client. Trong một dự án thật, Martin kể đội của ông không chạy database suốt 18 tháng đầu: không lo schema, không lo connection, và test chạy rất nhanh.

02 Database là một chi tiết

Cần phân biệt rạch ròi hai thứ thường bị gộp làm một:

Data model · QUAN TRỌNG

data model

Cấu trúc bạn tạo cho dữ liệu bên trong ứng dụng rất có ý nghĩa kiến trúc. Đây là điều cần thiết kế cẩn thận.

Database · CHI TIẾT

the database

Chỉ là một phần mềm tiện ích cấp truy cập dữ liệu — một "big bucket of bits" để lưu trên đĩa. Không được phép làm ô nhiễm kiến trúc.

The data is significant. The database is a detail.

Robert C. Martin · Chương 30

Relational cũng chỉ là một công cụ lưu trữ

Mô hình quan hệ (relational model) tuy thanh lịch và mạnh mẽ, vẫn chỉ là một storage technology. Một ứng dụng tốt không nên biết hay quan tâm dữ liệu được tổ chức dưới dạng bảng (tables) hay không — kiến thức về cấu trúc bảng chỉ nên nằm ở các hàm tiện ích cấp thấp nhất, vòng ngoài cùng.

Lỗi kiến trúc kinh điển: để các rowstables của database trôi khắp hệ thống dưới dạng object. Điều này ghép chặt (couple) use cases, business rules — đôi khi cả UI — vào cấu trúc quan hệ của dữ liệu.

Vậy còn performance?

Phải, performance là một mối quan tâm kiến trúc. Nhưng với lưu trữ, nó vẫn chỉ là chi tiết cấp thấp. Đĩa quay chậm (độ trễ mili-giây) là lý do RDBMS ra đời với index, cache, query tối ưu — nhưng kiến trúc nên giả định đĩa không tồn tại và dữ liệu luôn sẵn trong RAM. Hiệu năng không biện minh cho việc cho database rò rỉ vào business rules.

Khi đĩa biến mất và mọi thứ nằm trong RAM, bạn sẽ tổ chức dữ liệu thành linked list, cây, hash table và truy cập qua con trỏ — chứ không phải bảng + SQL. Thực ra bạn đã luôn đọc dữ liệu vào RAM rồi tái tổ chức cho tiện. Đó là bằng chứng database chỉ là cơ chế trung chuyển bit.

Giai thoại của Martin: có lúc RDBMS bị nhét vào hệ thống vì lý do marketing (khách hàng kỳ vọng phải có một cái "checkbox" database), chứ không vì nhu cầu kỹ thuật. Giải pháp đúng: "bắt vít" nó bên cạnh hệ thống qua một kênh truy cập hẹp & an toàn, giữ lõi sạch.

03 Web là một chi tiết

Web "thay đổi mọi thứ" trong thập niên 1990? Martin nói: không, hoặc lẽ ra không nên. Web chỉ là dao động mới nhất trong một con lắc đã đu đưa từ thập niên 1960 — giữa dồn sức tính toán về central serverđẩy ra terminal.

Mainframe + green-screen terminal

Sức mạnh tập trung ở máy chủ; terminal "ngu".

Server farm + "stupid browser"

Buổi đầu của web: mọi xử lý ở server, browser chỉ hiển thị.

Applets → quay về server

Đẩy code vào browser, rồi lại không thích, kéo nội dung động về server.

Web 2.0 · Ajax · JavaScript

Lại đẩy logic ra browser, dựng cả app khổng lồ chạy trong trình duyệt.

Node.js → kéo JS về server

Con lắc tiếp tục đu. "Web chỉ là một trong vô số dao động."

The GUI is a detail. The web is a GUI. So the web is a detail. Nói gọn: WEB là một IO device. Như những năm 1960 ta học cách viết ứng dụng device-independent, kiến trúc hôm nay cũng không nên để web chi phối.

Web chỉ là một delivery mechanism. Quyết định "app chạy trên web" nên được trì hoãn; kiến trúc phải càng vô tư càng tốt về việc nó được giao thế nào — để bạn có thể đổi sang console app, thick client hay web service mà không xáo trộn nền tảng. Cách làm: trừu tượng hóa ranh giới giữa UI và app thành các use case nhận input data trả output data qua data structure — vận hành UI như một IO device độc lập thiết bị.

04 Framework: hôn nhân bất đối xứng

Tác giả framework giải vấn đề của họ, không phải của bạn — chỉ là vấn đề thường chồng lấn đủ để framework hữu ích. Nhưng quan hệ giữa bạn và họ cực kỳ bất đối xứng (asymmetric).

Đây là một cuộc hôn nhân một chiều: bạn cam kết khổng lồ với framework, còn tác giả framework không cam kết gì với bạn cả. Bạn gánh mọi rủi ro và gánh nặng.

Asymmetric marriage · Chương 32

Tài liệu của framework thường khuyên bạn quấn kiến trúc của mình quanh nó: kế thừa base class của framework, import tiện ích của nó vào tận business object, ghép càng chặt càng tốt. Với tác giả, ghép chặt là không rủi ro — họ kiểm soát framework; và một khi bạn đã "đeo nhẫn cưới", rất khó dứt ra.

Rủi ro khi "cưới" framework

  • Framework hay vi phạm Dependency Rule — bắt bạn kế thừa code của nó vào chính Entities.
  • Đã vào vòng trong cùng thì không bao giờ ra; sản phẩm lớn lên có thể "outgrow" framework, lúc đó nó cản bạn.
  • Framework có thể tiến hóa lệch hướng, bỏ tính năng bạn đang dùng, hoặc bị thay bởi cái tốt hơn mà bạn không nhảy sang được.

Dùng đúng cách — giữ ở vòng ngoài

  • Don't marry the framework! Dùng được, nhưng đừng couple — giữ ở "arm's length".
  • Coi framework là detail thuộc vòng ngoài cùng; nếu nó đòi bạn kế thừa base class, hãy nói không — viết proxy thay thế, đặt proxy trong các plugin của business rules.
  • Ví dụ Spring: đừng rải @autowired khắp business object. Chỉ để Spring inject vào Main — thành phần "bẩn", cấp thấp nhất.

Frameworks are tools, not ways of life. Hãy nhìn mỗi framework bằng con mắt hoài nghi: nó có thể giúp, nhưng với cái giá nào? Tự hỏi cách dùng nó và cách tự bảo vệ trước nó — để giữ trọng tâm use-case của kiến trúc.

05 Tổ chức code: 4 cách & "Chương còn thiếu"

Trong The Missing Chapter, Simon Brown nhắc: mọi lời khuyên thiết kế đẹp đến đâu cũng có thể đổ vỡ ở phút chót — vì "the devil is in the implementation details". Lấy use case "xem trạng thái đơn hàng" của một book store, có 4 cách tổ chức code:

Cách tổ chứcChia theoĐiểm mạnh / yếu
Package by Layer
horizontal
Kỹ thuật: web · service · data Nhanh, đơn giản. Nhưng không "scream" domain — hai app khác hẳn nghiệp vụ trông giống nhau; lớn lên thì 3 "thùng" code không đủ.
Package by Feature
vertical
Khái niệm nghiệp vụ: gói orders Code "hét lên" về domain; dễ tìm mọi thứ cần sửa khi use case đổi. Một bước refactor nhẹ từ by-layer.
Ports & Adapters
hexagonal
"Inside" (domain) vs "outside" (infra) Domain độc lập với framework/DB; quy tắc: outside phụ thuộc inside, không bao giờ ngược lại.
Package by Component
hybrid
Gom mọi trách nhiệm của 1 component thô vào 1 package Gói business logic + persistence sau một interface sạch (vd OrdersComponent); giữ UI tách riêng. Hợp lối nhìn micro-service.

Organization vs Encapsulation — cạm bẫy public

Đây mới là điểm chí mạng. Developer hay dùng public theo quán tính cơ bắp. Nếu mọi type đều public, các package chỉ còn là cơ chế tổ chức (như thư mục) chứ không còn đóng gói (encapsulation) — vì type public gọi được từ bất cứ đâu.

Hệ quả: khi mọi thứ public, cả 4 cách kiến trúc trên trở nên giống hệt nhau. Người mới có thể "đi tắt" — gọi thẳng repository từ controller — phá thủng ranh giới mà chẳng gì ngăn cản. Ranh giới chỉ tồn tại trên sơ đồ, không tồn tại trong code.

Dựa vào kỷ luật

  • "Chúng tôi enforce bằng discipline và code review, vì tin developer."
  • Nhưng ai cũng biết chuyện gì xảy ra khi deadline & ngân sách ập tới gần.
  • Static analysis (NDepend, Checkstyle…) chỉ kiểm sau khi compile.

Dựa vào compiler

  • Dùng access modifier chặt hơn: package-protected (Java), internal (.NET).
  • Để implementation class (vd JdbcOrdersRepository) ẩn đi; chỉ interface cần thiết mới public.
  • Càng ít type public, càng ít phụ thuộc tiềm tàng — compiler tự enforce kiến trúc.

Case study Video Sales — lý thuyết thành hình

Chương 33 dựng một site bán video (kiểu cleancoders.com) để minh họa tất cả. Bắt đầu bằng nhận diện actors & use cases theo Single Responsibility Principle:

4 actor

SRP-driven

Author (nộp video, đề thi) · Purchaser (mua license) · Admin (đặt giá, series) · Viewer (xem/stream).

Component theo actor

Views · Presenters · Interactors · Controllers

Mỗi nhóm lại chia nhỏ theo actor; mọi phụ thuộc xuyên ranh giới trỏ về higher-level policy (Dependency Rule).

Hai chiều tách biệt trong sơ đồ: (1) tách actor theo SRP, (2) tách tầng policy theo Dependency Rule — cùng mục tiêu: cô lập những thứ đổi vì lý do khác nhau, ở nhịp khác nhau. Các component có thể đóng gói thành các .jar/.dll độc lập, rồi gộp lại linh hoạt khi cần (independent deployability).

Soi qua lăng kính RAG

RAG  Vector store là một detail. Core pipeline không nên biết nó nói chuyện với FAISS, pgvector hay Pinecone — chỉ biết một interface lưu/tìm vector. Đổi store = đổi 1 adapter, business rule bất động:

Python · vector store sau interface (database is a detail)
# core/ports.py — lõi chỉ biết cổng, không biết DB nào
class VectorStore(Protocol):
    def search(self, vec: list[float], k: int) -> list[Chunk]: ...

# core/retrieve.py — business rule, KHÔNG import faiss/pgvector
def retrieve(q: str, store: VectorStore, embed) -> list[Chunk]:
    return store.search(embed(q), k=5)        # đổi store = đổi adapter ngoài

# adapters/faiss_store.py — detail ở vòng ngoài cùng
class FaissStore:                              # implements VectorStore
    def search(self, vec, k): ...              # pgvector/Pinecone = adapter khác
Quyết định "dùng FAISS hay pgvector" được defer — y như Martin trì hoãn chọn Oracle/MySQL. Lõi retrieve viết & test được trước khi có database thật.

RAG  Framework là detail — giữ ở ranh giới service. Business rule không được import FastAPI hay Laravel; framework chỉ là lớp giao tiếp mỏng ở "Main", gọi vào lõi sạch:

Python · FastAPI ở vòng ngoài (don't marry the framework)
# main.py (vòng ngoài "bẩn") — framework chỉ ở đây
app = FastAPI()

@app.post("/ask")                              # @decorator không lọt vào core
def ask(req: AskRequest, svc: RagService = Depends(build)):
    return {"answer": svc.answer(req.q)}       # gọi vào lõi, lõi vô tư về FastAPI
RagService.answer() nằm trong core — không một dòng nào biết FastAPI. Thay bằng CLI hay gRPC = viết Main mới, lõi không đổi.

RAG  Package by component cho rag-core. Ở ranh giới HTTP, web Laravel chỉ gọi service Python qua một cổng hẹp — đúng tinh thần "bắt vít detail bên cạnh", giữ component lõi sau một interface sạch:

PHP · Laravel ở ranh giới service, chỉ gọi qua HTTP
// app/Services/RagClient.php — adapter mỏng sang component rag-core
class RagClient {
    public function ask(string $q): array {
        // web KHÔNG biết FAISS, embedding model hay prompt bên trong
        return Http::post(config('rag.url').'/ask', ['q' => $q])->json();
    }
}
// Controller chỉ chạm RagClient — không "đi tắt" vào lõi retrieve
Cổng /ask là interface công khai duy nhất của component rag-core; mọi chi tiết (vector store, model, framework) bị ẩn sau nó — encapsulation thắng organization.

06 Ghi nhớ nhanh & hoàn thành

Data is significant, the database is a detail. Thiết kế kỹ data model; coi DB (kể cả relational) là cơ chế lưu trữ vòng ngoài — đừng để rows/tables trôi khắp hệ thống.

The web is an IO device. GUI/web chỉ là delivery mechanism; cô lập sau boundary, để use case vận hành độc lập thiết bị. Trì hoãn quyết định "chạy trên web".

Don't marry the framework. Dùng như công cụ, giữ ở vòng ngoài qua proxy/plugin; business rule không biết tên framework. Framework là tool, không phải lối sống.

Encapsulation over organization. Lạm dụng public biến mọi kiến trúc thành như nhau. Lean on the compiler — dùng package-protected/internal để ranh giới có thật trong code.

The devil is in the implementation details. Ý đồ thiết kế đẹp vẫn sụp nếu cách tổ chức code không khớp. Map design xuống code structure một cách tỉnh táo.

Hoàn thành cuốn sách. Từ "hai giá trị" của phần mềm, qua SOLID, component, boundaries, các tầng policy — tới đây mọi mảnh ghép hội tụ: giữ business rules thuần khiết ở lõi, đẩy mọi chi tiết ra rìa, và bảo vệ ranh giới bằng chính compiler. Đó là Clean Architecture.

NguồnChương 30–34 (The Database/Web/Frameworks Are Details, Case Study: Video Sales, The Missing Chapter), Clean Architecture — Robert C. Martin, Prentice Hall 2017.