Clean Architecture
05 Phần III · Component Chương 12 & 13

Component & Cohesion

Components · Cohesion (REP · CCP · CRP)

Sau khi SOLID dạy cách sắp xếp lớp, câu hỏi tiếp theo nâng lên một tầm: lớp nào nên thuộc về component nào? Component là đơn vị triển khai thật của hệ thống — và việc phân chia chúng được dẫn lối bởi ba nguyên lý kết dính giằng co lẫn nhau: REP, CCP và CRP.

01 Component là gì

Component là đơn vị triển khai (units of deployment) — những thực thể nhỏ nhất có thể được triển khai như một phần của hệ thống. Trong Java là .jar, Ruby là .gem, .Net là .dll. Bất kể ngôn ngữ nào, chúng là hạt của việc triển khai.

Component thiết kế tốt luôn giữ được khả năng triển khai độc lập (independently deployable) — và do đó phát triển độc lập (independently developable). Đó là phẩm chất quyết định, không phải cách cuối cùng nó được đóng gói.

50 năm để component plugin thành "mặc định"

Khả năng cắm component như plugin ngày nay là chuyện hiển nhiên — nhưng nó là thành quả của một hành trình dài về tính tái định vị (relocatability):

Không thể tái định vị

Thời kỳ đầu, lập trình viên phải tự khai báo địa chỉ tải tuyệt đối (vd *200). Thư viện được nhúng source thẳng vào chương trình rồi biên dịch chung — chương trình non-relocatable.

Tách thư viện · relocating loader

Biên dịch chậm vì bộ nhớ đắt, phải đọc source nhiều lượt. Người ta tách thư viện ra biên dịch riêng; rồi relocating loader ra đời — tự cộng địa chỉ nền vào các tham chiếu bộ nhớ, cho tải mã ở bất kỳ đâu.

Tách Linker khỏi Loader

Linking loader quá chậm khi phải đọc hàng trăm thư viện. Khâu liên kết được tách thành linker riêng, xuất ra một linked relocatable mà loader nạp rất nhanh.

Kiến trúc plugin

Giữa thập niên 1990, phần cứng đủ nhanh để liên kết ngay lúc nạp (load time) trong vài giây. Từ đó ta cắm .jar/.dll linh hoạt — component plugin architecture trở thành mặc định.

Chính những tệp liên kết động (dynamically linked), cắm được lúc chạy này là các software component của kiến trúc hiện đại — từ mod Minecraft tới plugin Resharper trong Visual Studio.

02 REP — Tái sử dụng = Phát hành

Reuse/Release Equivalence Principle: "Hạt nhân của tái sử dụng là hạt nhân của phát hành" (the granule of reuse is the granule of release). Muốn một thứ được tái sử dụng, nó phải được phát hành (release) đàng hoàng — và được theo dõi bằng số phiên bản.

Phải có số phiên bản

release / version number

Không có release number thì không có cách nào đảm bảo các component tái dùng tương thích với nhau. Maven, Leiningen, RVM… sinh ra để quản lý chính điều này.

Phải có tài liệu phát hành

release documentation

Người dùng cần biết trước bản mới mang theo thay đổi gì để quyết định nâng cấp — hay ở lại bản cũ. Quy trình release phải phát thông báo rõ ràng.

Về mặt thiết kế, REP đòi hỏi các lớp trong một component phải thuộc về một nhóm gắn kết — có chung một chủ đề/mục đích bao trùm (overarching theme), có thể phát hành cùng nhau một cách hợp lý. Không phải một mớ lớp gom ngẫu nhiên.

REP qua lăng kính RAG

RAG  Web PHP (Laravel) gọi sang Python service qua HTTP. Hợp đồng schema giữa hai bên chính là một "release": đổi shape của response = bump số phiên bản, để bên PHP biết khi nào phải nâng cấp.

Python · rag-service · phát hành interface có version
# rag-service công bố schema KÈM số phiên bản — đây là "release"
RAG_API_VERSION = "2.3.0"          # đổi schema = bump release number

@app.post("/v2/answer")
def answer(req: AskV2) -> AnswerV2:   # AnswerV2: {text, citations[], schema_version}
    return AnswerV2(text=..., citations=..., schema_version=RAG_API_VERSION)
Bên Laravel pin một dải version tương thích; khi service phát hành 3.0.0 (đổi shape), tài liệu release báo trước để PHP quyết định nâng cấp hay ở lại /v2.
PHP · ranh giới service · kiểm version đã phát hành
// PHP CHỈ ở ranh giới: gọi service, đối chiếu version đã pin
$res = Http::post(config('rag.url').'/v2/answer', $payload)->json();

abort_unless(
    Semver::satisfies($res['schema_version'], '^2.3'),  // tương thích bản đã phát hành?
    502, 'rag-service trả schema ngoài dải đã phát hành'
);
Số phiên bản là "hạt nhân của phát hành" — không có nó, hai bên không thể biết mình còn tương thích hay không.

03 CCP — SRP ở tầm component

Gom vào cùng một component những lớp thay đổi vì cùng một lý do và tại cùng một thời điểm. Tách ra component khác những lớp đổi vì lý do khác, thời điểm khác.

Common Closure Principle · Clean Architecture

Common Closure Principle chính là SRP phát biểu lại cho component: như SRP nói một lớp không nên có nhiều lý do để đổi, CCP nói một component không nên có nhiều lý do để đổi.

Với hầu hết ứng dụng: bảo trì > tái dùng

maintainability over reusability

Khi yêu cầu đổi, ta muốn mọi thay đổi gói gọn trong MỘT component thay vì rải khắp nơi — giảm công phát hành, kiểm thử lại và tái triển khai.

Họ hàng với OCP

Open–Closed Principle

Vì "đóng" 100% là bất khả, đóng phải có chiến lược. CCP gom vào một chỗ những lớp cùng đóng với một loại thay đổi — yêu cầu mới đổi tới chỉ chạm số component tối thiểu.

CCP qua lăng kính RAG

RAG  Trong pipeline, chunking + embedding luôn đổi cùng nhau vì cùng một lý do (đổi model embedding, đổi chiến lược cắt văn bản). CCP bảo: gom chúng vào một component rag-index, tách khâu truy hồi ra component khác (truy hồi đổi vì lý do riêng: đổi vector store, đổi top-k).

Python · rag-index/ · gom thứ đổi-cùng-lý-do
# rag-index/ — chunking & embedding đổi CÙNG lý do ⇒ cùng component (CCP)
rag_index/
    chunking.py      # đổi khi đổi chiến lược cắt văn bản
    embedding.py     # đổi khi đổi model embedding   ⟵ cùng nhịp với chunking

# rag-retrieval/ — đổi vì lý do KHÁC ⇒ tách riêng
rag_retrieval/
    vector_store.py  # đổi khi đổi Qdrant ↔ pgvector
    ranker.py        # đổi khi đổi chiến lược top-k / rerank
Khi yêu cầu "đổi model embedding" tới, chỉ rag-index bị phát hành lại — đúng tinh thần CCP: thu hẹp ảnh hưởng thay đổi vào số component ít nhất.

04 CRP — ISP ở tầm component

Đừng ép người dùng một component phải phụ thuộc vào những thứ họ không cần.

Common Reuse Principle · Clean Architecture

Common Reuse Principlephiên bản tổng quát của ISP: ISP khuyên đừng phụ thuộc vào lớp có phương thức ta không dùng; CRP khuyên đừng phụ thuộc vào component có lớp ta không dùng. Rút gọn cả hai thành một câu: "Đừng phụ thuộc vào thứ bạn không cần."

Lớp NÊN ở chung

  • Các lớp thường được tái dùng cùng nhau — hiếm khi dùng cô lập, chúng cộng tác trong cùng một trừu tượng.
  • Trong component lý tưởng, các lớp phụ thuộc chặt vào nhau, gần như không tách rời.

Lớp KHÔNG nên ở chung

  • Lớp không gắn bó chặt không nên nằm chung component.
  • Nếu lẫn vào, mỗi lần thứ-không-liên-quan đổi sẽ buộc tái triển khai vô ích, phí công.

CRP nói nhiều về lớp nào KHÔNG nên ở cùng nhau hơn là lớp nào nên. Khi phụ thuộc vào một component, ta muốn phụ thuộc vào mọi lớp trong nó — chúng phải bất khả phân.

CRP qua lăng kính RAG

RAG  Gói rag-core chỉ nên chứa thứ luôn được dùng chung ở runtime (loại tài liệu, cổng embedder/retriever). Đừng nhét cli hay eval vào — bên web PHP gọi service không cần chúng; kéo theo = mỗi lần đổi tool eval lại buộc phát hành lại rag-core.

Python · pyproject · tách rag-core khỏi cli/eval
# rag-core: CHỈ thứ runtime luôn dùng-chung-nhau (CRP)
rag_core/        →  document.py, embedder.py, retriever.py
# rag-cli, rag-eval: tách thành package riêng, KHÔNG nằm trong rag-core
[project.optional-dependencies]
cli  = ["rag-cli"]    # chỉ ai cần dòng lệnh mới kéo
eval = ["rag-eval"]   # chỉ pipeline đo lường mới kéo
Web PHP chỉ phụ thuộc rag-core ⇒ đổi công cụ eval không buộc service phát hành lại. Phụ thuộc đúng thứ mình cần — không hơn.

05 Tam giác căng thẳng REP/CCP/CRP

Ba nguyên lý trên giằng co lẫn nhau. REP và CCP là nguyên lý bao hàm (inclusive) — kéo component to ra. CRP là nguyên lý loại trừ (exclusive) — đẩy component nhỏ lại. Kiến trúc sư giỏi là người tìm được điểm cân bằng trong sức căng đó.

REP Tái sử dụng = Phát hành CCP SRP tầm component CRP ISP tầm component bỏ REP → khó tái sử dụng bỏ CRP → quá nhiều bản phát hành thừa bỏ CCP → quá nhiều component bị đổi REP·CCP: to ra (inclusive) CRP: nhỏ lại (exclusive)
Mỗi cạnh ghi cái giá khi bỏ qua nguyên lý ở đỉnh ĐỐI DIỆN. Bám REP+CRP mà bỏ CCP → thay đổi nhỏ cũng động tới quá nhiều component. Bám CCP+REP mà bỏ CRP → sinh quá nhiều bản phát hành không cần thiết. Theo thời gian: dự án non thường ưu tiên CCP (gom thứ đổi cùng nhau); khi trưởng thành & được nhiều nơi tái dùng, trọng tâm dịch dần sang REP/CRP.

Vị trí trong tam giác dịch theo độ trưởng thành dự án. Dự án non trẻ nằm bên phải (CCP) — coi trọng khả năng phát triển (develop-ability) hơn tái sử dụng, hy sinh duy nhất là reuse. Khi trưởng thành và được dự án khác dùng lại, nó trượt dần sang trái (CRP/REP). Cách phân chia component vì thế không cố định — nó dao động và tiến hóa theo thời gian.

06 Ghi nhớ nhanh

Component = đơn vị triển khai nhỏ nhất (jar/dll/gem). Phẩm chất cốt lõi: triển khai & phát triển độc lập. Kiến trúc plugin hiện đại dựa trên liên kết động lúc chạy.

REP: hạt nhân của tái sử dụng là hạt nhân của phát hành — muốn tái dùng thì phải có số phiên bản & tài liệu release.

CCP = SRP tầm component: gom thứ đổi-cùng-lý-do-cùng-lúc; thu hẹp ảnh hưởng thay đổi vào số component tối thiểu.

CRP = ISP tầm component: đừng ép phụ thuộc vào thứ không cần; nói rõ hơn về lớp nào không nên ở chung.

Tam giác căng thẳng: REP/CCP kéo to, CRP kéo nhỏ. Dự án non nghiêng CCP (develop-ability), trưởng thành thì trượt sang CRP/REP (reuse) — cân bằng luôn động.

NguồnChương 12 (Components) & Chương 13 (Component Cohesion), Clean Architecture — Robert C. Martin, Prentice Hall 2017.