Clean Architecture
07 Phần IV · Kiến trúc Chương 15 & 16

Kiến trúc là gì & Tính độc lập

What Is Architecture · Independence

Kiến trúc một hệ thống chính là HÌNH DẠNG (shape) mà người xây dựng tạo ra: cách chia component & cách chúng giao tiếp. Mục tiêu không phải khiến hệ thống "chạy đúng" — mà giữ cho nó mềm dẻo nhất có thể, càng lâu càng tốt, bằng cách để mở các lựa chọn. Người làm kiến trúc giỏi nhất vẫn là một lập trình viên.

01 Kiến trúc là gì — và ai làm nó

Kiến trúc của một hệ thống phần mềm là HÌNH DẠNG (shape) mà những người xây dựng nó tạo ra — qua cách chia hệ thống thành component, cách sắp xếp, và cách chúng giao tiếp.

Robert C. Martin · Chương 15

Có thể bạn nghĩ mục tiêu của kiến trúc là "làm hệ thống chạy đúng". Tất nhiên ta muốn nó chạy đúng — nhưng architecture rất ít chi phối behavior. Điều quan trọng nhất kiến trúc làm được cho hành vi là phơi bày ý đồ (intent) của hệ thống ra cấp độ cấu trúc, để lập trình viên nhìn là thấy.

Shape = (cách chia component) × (cách sắp xếp) × (cách chúng giao tiếp). Mục tiêu tối thượng: tối thiểu hóa chi phí vòng đời & tối đa hóa năng suất lập trình viên — không phải số tính năng.

Kiến trúc sư vẫn là lập trình viên

Đừng tin lời nói dối rằng kiến trúc sư "rút khỏi code để lo việc cấp cao". Họ không hề. Kiến trúc sư là những lập trình viên giỏi nhất và tiếp tục nhận task lập trình. Lý do: họ không thể làm tốt việc của mình nếu không tự nếm trải những vấn đề mà các quyết định kiến trúc của họ gây ra cho phần còn lại của đội.

02 Để mở các lựa chọn: Policy vs Detail

Phần mềm được phát minh để "soft"-ware — dễ đổi hành vi máy móc. Giữ cho nó mềm nghĩa là để mở càng nhiều lựa chọn càng tốt, càng lâu càng tốt (leaving options open), bằng cách trì hoãn các quyết định về những chi tiết không quan trọng.

Policy

business rules & procedures

Mọi quy tắc và quy trình nghiệp vụ — nơi chứa giá trị thật của hệ thống. Đây là cái bạn phải bảo vệ.

Detail

DB · framework · web · IO

Thứ cần để con người/hệ thống khác giao tiếp với policy, nhưng không ảnh hưởng hành vi của policy: database, web server, framework, giao thức, IO.

Mục tiêu của kiến trúc sư: tạo shape sao cho detail trở nên không liên quan (irrelevant) với policy — nhờ vậy quyết định về DB, web framework, IO… được trì hoãn tới phút chót. Một kiến trúc sư giỏi tối đa hóa số quyết định CHƯA phải đưa ra (maximizes the number of decisions not made).

Không cần chọn loại database sớm: policy cấp cao lẽ ra không quan tâm nó là relational, distributed, hierarchical hay flat file. Không cần chọn web server sớm: policy không nên biết mình đang được phân phối qua web. Lỡ công ty đã "chốt" một DB/framework rồi? Kiến trúc sư giỏi vờ như chưa chốt, và shape hệ thống sao cho quyết định ấy vẫn có thể đổi càng lâu càng tốt.

Device independence (thập niên 1960): OS trừu tượng hóa thiết bị IO thành "unit-record" ảo. Chương trình viết một lần, test trên line printer, rồi "in" ra băng từ. Shape ấy tách policy (định dạng bản ghi) khỏi detail (thiết bị) — chính là nguyên lý hôm nay áp dụng ở quy mô lớn: tách policy khỏi đĩa cứng, khỏi loại DB.

03 Bốn thứ một kiến trúc tốt phải hỗ trợ

Một kiến trúc tốt phải đồng thời nâng đỡ vòng đời hệ thống trên bốn mặt — và cân bằng tất cả bằng một cấu trúc component thỏa mãn lẫn nhau. Nghe thì dễ; làm mới khó.

Khía cạnhKiến trúc làm gì
Use cases
& operation
Hỗ trợ intent của hệ thống (giỏ hàng phải hỗ trợ use case giỏ hàng) — ưu tiên số một; và đáp ứng throughput / response time mà vận hành đòi hỏi.
OperationGiữ component cô lập tốt để dễ chuyển giữa monolith ↔ đa luồng ↔ đa tiến trình ↔ micro-services khi nhu cầu đổi. Hình thái thực thi là một lựa chọn để mở.
DevelopmentPhân vùng thành component cô lập, độc lập phát triển — nhiều đội làm song song không cản nhau (Conway's law).
DeploymentBuild xong triển khai được ngay (immediately deployable), không cần kịch bản cấu hình rườm rà hay tạo thủ công thư mục/file.

Maintenance là khía cạnh tốn kém nhất. Tách hệ thống thành component và cô lập qua giao diện ổn định (stable interfaces) sẽ soi sáng đường cho tính năng mới và giảm mạnh rủi ro vỡ dây chuyền.

04 Decoupling: tầng ngang vs use case dọc

Kiến trúc tốt khiến hệ thống dễ thay đổi theo mọi hướng nó phải đổi, bằng cách để mở các lựa chọn. Có hai trục tách rời, và một câu hỏi thứ ba về mức độ tách.

Decoupling Layers

horizontal · ngang

Tách theo SRP + CCP: UI · application-specific rules · application-independent rules · database. Mỗi tầng đổi với tốc độ và lý do khác nhau.

Decoupling Use Cases

vertical · dọc

Mỗi use case là lát cắt dọc xuyên qua các tầng (addOrder ≠ deleteOrder). Tách dọc ⟶ thêm use case mới không động vào cái cũ.

flowchart TB
  subgraph L["Tách NGANG · Decoupling Layers"]
    direction TB
    UI["UI"]:::core --> APP["App-specific rules"]:::mid --> DOM["App-independent rules"]:::mid --> DB["Database"]:::out
  end
  subgraph V["Tách DỌC · Decoupling Use Cases"]
    direction LR
    U1["addOrder
(lát cắt)"]:::slice U2["deleteOrder
(lát cắt)"]:::slice U3["viewReport
(lát cắt)"]:::slice end L --> V classDef core fill:#dce7f1,stroke:#2e5e8c,color:#14233b; classDef mid fill:#e2edf3,stroke:#2f6d93,color:#14233b; classDef out fill:#e2edf3,stroke:#2f6d93,color:#14233b; classDef slice fill:#dce7f1,stroke:#2e5e8c,color:#14233b;
Use case là lát cắt dọc đâm xuyên các tầng ngang. Mỗi lát dùng một phần UI, một phần business rules, một phần DB riêng — nên independent developability & independent deployability: có thể hot-swap layer/use case bằng vài jar/service mà không đụng phần còn lại.

Decoupling Modes — tách tới mức nào?

Source level

Kiểm soát phụ thuộc giữa module nguồn để đổi một module không buộc biên dịch lại module khác (vd Ruby Gems). Tất cả chạy cùng một address space, gọi nhau bằng function call — đây chính là monolith.

Deployment level

Kiểm soát phụ thuộc giữa các đơn vị triển khai (jar, DLL, shared library) để đổi một module không buộc rebuild/redeploy module khác. Nhiều component vẫn có thể chung một tiến trình.

Service level

Hạ phụ thuộc xuống mức cấu trúc dữ liệu, giao tiếp qua network packet (services / micro-services). Mỗi đơn vị thực thi hoàn toàn độc lập với thay đổi source & binary của đơn vị khác.

Đừng mặc định service-level. Mặc định micro-services thì đắt (thời gian dev + tài nguyên) và đẩy về tách thô (coarse-grained). Ưu tiên của Martin: đẩy việc tách tới điểm CÓ THỂ tạo service nếu cần, nhưng giữ component trong cùng address space lâu nhất có thể. Hệ thống có thể sinh ra là monolith → lớn thành deployable units → thành services → rồi trượt ngược về monolith. Decoupling mode là một lựa chọn để mở, sẽ đổi theo thời gian.

RAG · use case dọc & ranh giới service

RAG  Hai use case "ingest tài liệu" và "hỏi đáp" đổi vì lý do khác nhau → tách thành hai lát dọc riêng, mỗi lát có pipeline riêng:

Python · hai use case = hai lát dọc độc lập
# Lát dọc 1 · INGEST — đổi khi format tài liệu / chiến lược chunk đổi
class IngestUseCase:
    def run(self, doc) -> None:
        chunks = self.splitter.split(doc)        # UI/intake riêng
        self.vector_store.upsert(self.embed(chunks))

# Lát dọc 2 · ASK — đổi khi cách retrieve / prompt trả lời đổi
class AskUseCase:
    def run(self, question) -> str:              # độc lập với Ingest
        ctx = self.vector_store.search(question)
        return self.llm.generate(self.prompt(ctx, question))
Thêm use case "tóm tắt tài liệu" sau này chỉ là thêm một lát dọc mới — không phải mổ lại Ingest hay Ask. Đó là independent developability.

RAG  Ranh giới PHP (web) ↔ Python (pipeline) đi qua network = service-level decoupling sẵn có. Nhưng đừng vì thế mà chẻ tiếp pipeline Python thành 5 micro-service "cho hiện đại":

PHP · chỉ ở ranh giới service, gọi sang qua HTTP
// PHP CHỈ ở ranh giới — không nhúng logic pipeline
$res = Http::post('http://rag-svc/ask', ['q' => $question]);
return $res->json('answer');   // service-level: độc lập source & binary
// Bên trong rag-svc, Ingest & Ask vẫn chung 1 address space (source-level)
// → giữ option "tách thành service riêng" để mở, chưa tốn ngay.
Một ranh giới service ở đúng chỗ (PHP↔Python) là đủ. Bên trong Python giữ source-level cho rẻ, để mở lựa chọn nâng cấp về sau.

05 Trùng lặp THẬT vs trùng lặp ngẫu nhiên

Kiến trúc sư hay sa vào một cái bẫy: sợ trùng lặp. Nhưng có hai loại trùng lặp rất khác nhau — và gộp nhầm loại thứ hai sẽ tạo coupling không đáng có.

True duplication

  • Mọi thay đổi ở bản này BẮT BUỘC lặp lại y hệt ở bản kia.
  • Chúng thay đổi cùng nhau, vì cùng một lý do nghiệp vụ.
  • Đây mới là trùng lặp đáng gộp (unify).

Accidental / false duplication

  • Hai đoạn trông giống nhau nhưng tiến hóa theo đường khác nhau — đổi với tốc độ & lý do khác nhau.
  • Vài năm sau quay lại sẽ thấy chúng đã rất khác nhau.
  • Gộp vội ⟶ tách lại sau này là cực hình.

Hai use case có màn hình giống nhau, hay cấu trúc database record giống hệt screen view — phần lớn là trùng lặp ngẫu nhiên. Đừng tuồn thẳng record DB lên UI; hãy tạo view model riêng. Chống lại cám dỗ "knee-jerk elimination of duplication" — chỉ gộp khi chắc chắn là true duplication.

RAG · cùng format prompt ≠ phải gộp

RAG  Use case "ingest" và "ask" tình cờ cùng dựng một prompt header trông giống nhau. Đừng vội rút ra hàm dùng chung — chúng sẽ tiến hóa khác hướng:

Python · accidental duplication — giữ tách
# Trông giống nhau HÔM NAY, nhưng đổi vì lý do khác nhau → KHÔNG gộp vội
def ingest_prompt(chunk):   # đổi khi tinh chỉnh cách mô tả tài liệu để embed
    return f"Tài liệu:\n{chunk}"

def ask_prompt(ctx, q):     # đổi khi tinh chỉnh giọng trả lời, chống hallucination
    return f"Ngữ cảnh:\n{ctx}\n\nCâu hỏi: {q}"
# Gộp lại = ép hai use case coupling; chỉ unify nếu CHẮC là true duplication.
Có thể giữ tách rồi gộp dần khi thấy chúng thật sự luôn-đổi-cùng-nhau — chứ không phải ngược lại.

06 Ghi nhớ nhanh

Kiến trúc = SHAPE (cách chia + sắp xếp + cách component giao tiếp). Kiến trúc sư giỏi nhất vẫn là lập trình viên — phải tự nếm vấn đề mình tạo ra.

Để mở các lựa chọn: tách policy (giá trị nghiệp vụ) khỏi detail (DB/web/framework/IO), trì hoãn quyết định chi tiết tới phút chót — tối đa hóa số quyết định CHƯA phải đưa ra.

Tách hai trục: layers ngang (UI/rules/DB) + use cases dọc (lát cắt). Tách tốt ⟶ phát triển & triển khai độc lập, hot-swap được.

Decoupling mode (source → deployment → service) là lựa chọn để mở: đẩy tới điểm CÓ THỂ tạo service, nhưng giữ cùng address space lâu nhất có thể. Mode sẽ đổi theo thời gian.

Đừng gộp trùng lặp ngẫu nhiên. Chỉ unify khi chắc là true duplication (luôn đổi cùng nhau vì cùng lý do). Giống tạm thời ⟶ giữ tách.

NguồnChương 15 (What Is Architecture?) & Chương 16 (Independence), Clean Architecture — Robert C. Martin, Prentice Hall 2017.