Clean Architecture
02 Phần I · Nền tảng Chương 3–6

Ba Paradigm lập trình

Programming Paradigms

Chỉ có ba paradigm — Structured, Object-Oriented, Functional — và tất cả ra đời gọn trong 1958–1968. Điều bất ngờ: mỗi paradigm không thêm quyền năng nào cho lập trình viên, mà lấy đi một thứ (goto · con trỏ hàm · phép gán). Chúng là những rào cản tích cực — dạy ta điều không được làm để giữ cấu trúc phần mềm sạch.

01 Ba paradigm: mỗi cái lấy đi một thứ

Rào cản tích cực, không phải công cụ mới

Một paradigm (hướng lập trình) cho ta biết dùng cấu trúc lập trình nào, khi nào dùng — gần như độc lập với ngôn ngữ. Đến nay chỉ có ba, và nhiều khả năng sẽ không bao giờ có thêm. Lý do nằm ở một quy luật chung mà Martin cố tình bày ra:

Mỗi paradigm lấy đi một quyền năng của lập trình viên. Không cái nào thêm quyền năng mới. Chúng nói cho ta biết điều không được làm, nhiều hơn là điều nên làm.

Clean Architecture · Chương 3
ParadigmNămLấy đi quyền năng…Áp kỷ luật lên…Lo mối quan tâm kiến trúc
Structured
Dijkstra · 1968
1968 goto — chuyển điều khiển trực tiếp (direct transfer of control) Luồng điều khiển trực tiếp Chức năng (function)
Object-Oriented
Dahl & Nygaard · 1966
1966 con trỏ hàm — chuyển điều khiển gián tiếp (indirect transfer of control) Luồng điều khiển gián tiếp Chia tách thành phần (separation of components)
Functional
λ-calculus · Church 1936
1958 phép gán (assignment) Phép gán / trạng thái biến Quản lý dữ liệu (data management)

Cả ba được phát hiện gói gọn trong mười năm 1958–1968; nhiều thập kỷ sau, không có paradigm nào mới. Vì sao? Vì không còn quyền năng nào để lấy đi nữa — chỉ còn goto, con trỏ hàm và phép gán để cấm, và cả ba đều đã bị cấm.

Ba mối quan tâm này chính là xương sống của cả cuốn sách: Structured lo cách viết function, OO lo cách chia tách & nối ghép component, Functional lo cách quản lý dữ liệu & trạng thái.

02 Structured Programming

goto có hại — và vì sao

Năm 1968, Edsger Dijkstra chỉ ra rằng việc dùng goto bừa bãi (unrestrained jumps) có hại cho cấu trúc chương trình. Vấn đề không phải thẩm mỹ: một số cách dùng goto khiến module không thể phân rã đệ quy thành các đơn vị nhỏ hơn — chặn mất chiến lược chia-để-trị cần thiết để chứng minh tính đúng.

Dijkstra phát hiện những cách dùng goto "tốt" lại đúng bằng các cấu trúc lựa chọn (if/then/else) và lặp (do/while). Và Böhm–Jacopini đã chứng minh: mọi chương trình đều dựng được chỉ từ ba cấu trúc — tuần tự, lựa chọn, lặp (sequence, selection, iteration). Đúng bộ tối thiểu khiến module chứng minh được lại là bộ đủ để dựng mọi thứ. Structured programming ra đời.

Tuần tự Lựa chọn Lặp
Toàn bộ phần mềm — từ 1946 tới nay — chỉ là tuần tự · lựa chọn · lặp · chuyển hướng gián tiếp (sequence, selection, iteration, indirection). Không hơn, không kém.

Phân rã chức năng & chứng minh được

Vì cho phép phân rã đệ quy thành đơn vị chứng minh được, structured programming sinh ra functional decomposition: tách bài toán lớn thành function cấp cao, rồi cấp thấp hơn, mãi mãi — mỗi mảnh đều dùng đúng bộ cấu trúc hạn chế ấy. Ở tầm kiến trúc, đây vẫn là một best practice: chia hệ thống thành module, component, service nhỏ và dễ kiểm thử.

Giấc mơ "chứng minh đúng kiểu Euclid" của Dijkstra không thành. Thực tế: phần mềm giống khoa học (science), không phải toán học. Khoa học không chứng minh điều gì đúng, chỉ chứng minh điều gì sai (falsifiable). Test không chứng minh code đúng — nó chỉ cố chứng minh code sai; cố mãi mà không được thì ta tạm coi là "đủ đúng cho mục đích". Và chỉ chương trình chứng minh được (đã bỏ goto bừa bãi) mới kiểm thử được như vậy.

03 Object-Oriented & đảo ngược phụ thuộc

Ba "từ khóa ma thuật" — và cái duy nhất quan trọng

OO hay được định nghĩa bằng encapsulation · inheritance · polymorphism. Martin lập luận thẳng: hai cái đầu bị thổi phồng — C đã có encapsulation hoàn hảo, và OO thực ra còn làm yếu nó đi; inheritance chỉ là thủ thuật C đã làm được từ trước. Với kiến trúc sư, chỉ một thứ thật sự đáng giá:

Encapsulation & Inheritance

  • C có đóng gói hoàn hảo; public/private chỉ là bản vá vì compiler cần thấy biến trong header.
  • Java/C# bỏ tách header/implementation ⟶ đóng gói còn yếu hơn.
  • Inheritance: chỉ là khai báo lại nhóm biến/hàm trong scope bao — lập trình viên C đã làm thủ công.

Polymorphism — chìa khóa

  • Bản chất chỉ là con trỏ hàm — có từ thập niên 1940.
  • Nhưng con trỏ hàm thủ công thì nguy hiểm: quên khởi tạo/gọi đúng quy ước là sinh bug khó truy.
  • OO khiến polymorphism an toàn & tiện ⟶ áp kỷ luật lên chuyển điều khiển gián tiếp.

Đảo ngược phụ thuộc (Dependency Inversion)

Trong cây gọi truyền thống, phụ thuộc mã nguồn luôn đi cùng hướng với luồng điều khiển: để gọi một hàm, bạn buộc phải #include/import tên module chứa nó. Kiến trúc sư gần như không có lựa chọn. Nhờ polymorphism an toàn, ta chèn một interface vào giữa, và phụ thuộc mã nguồn (quan hệ kế thừa) chỉ ngược hướng với luồng điều khiển:

flowchart LR
  HL["HL1
(business rule)"]:::core -->|"luồng điều khiển →"| F["F() trong ML1
(chi tiết: DB/UI)"]:::out ML["ML1"]:::out -.->|"phụ thuộc mã nguồn ↩ ngược hướng"| I(["interface I"]):::core classDef core fill:#dbeee8,stroke:#0f7d72,color:#14233b; classDef out fill:#e2edf3,stroke:#2f6d93,color:#14233b;
Luồng điều khiển (nét liền) vẫn từ business rule gọi xuống chi tiết; nhưng phụ thuộc mã nguồn (nét đứt) bị đảo hướng: ML1 phụ thuộc vào interface do tầng cao định nghĩa. Tại runtime interface không tồn tại — nó là một "thủ thuật mã nguồn".

Với OO, bất kỳ phụ thuộc mã nguồn nào cũng đảo được bằng cách cắm một interface vào giữa. Kiến trúc sư có quyền kiểm soát tuyệt đối hướng của mọi phụ thuộc trong hệ thống — không còn bị luồng điều khiển ép buộc. Đó mới là OO, dưới góc nhìn kiến trúc.

Quyền năng đó cho phép sắp xếp để UI và Database phụ thuộc vào business rule, chứ không phải ngược lại. Khi đó UI và DB trở thành plugin cắm vào business rule; mã nguồn business rule không bao giờ nhắc tới UI hay DB. Ba thứ biên dịch thành ba đơn vị triển khai tách rời ⟶

Plugin Architecture

plugin architecture

Chi tiết cấp thấp (UI, DB) thành plugin cho business rule cấp cao — cắm/rút mà không động tới lõi.

Độc lập triển khai

independent deployability

Đổi mã trong một component ⟶ chỉ component đó cần deploy lại. Sửa UI/DB không đụng business rule.

Độc lập phát triển

independent developability

Deploy độc lập ⟹ phát triển độc lập — các đội khác nhau làm các thành phần khác nhau, không giẫm chân.

Dependency inversion qua lăng kính RAG

RAG  Hệ Hỏi–đáp tài liệu cần embedlưu/truy hồi vector. Nếu pipeline gọi thẳng FAISS, đổi sang pgvector là sửa khắp nơi. Định nghĩa interface ở tầng pipeline (business rule) rồi để FAISS/pgvector cắm vào như plugin — phụ thuộc mã nguồn đảo hướng đúng tinh thần OO:

Python · interface ở lõi, vector store là plugin
from typing import Protocol

# Lõi pipeline ĐỊNH NGHĨA interface — chi tiết phải tuân theo (phụ thuộc đảo hướng)
class Embedder(Protocol):
    def embed(self, text: str) -> list[float]: ...

class VectorStore(Protocol):
    def search(self, vec: list[float], k: int) -> list[str]: ...

def retrieve(q: str, emb: Embedder, store: VectorStore) -> list[str]:
    return store.search(emb.embed(q), k=5)   # đổi FAISS↔pgvector = thay plugin, lõi không đổi
Lõi retrieve không hề nhắc tên FAISS hay pgvector. Vector store giờ là plugin — đúng nghĩa "low-level details cắm vào high-level policy".

04 Functional Programming & tính bất biến

"Biến" trong ngôn ngữ chức năng không hề biến đổi

Functional programming là paradigm ra đời sớm nhất về ý tưởng (λ-calculus của Alonzo Church, 1936) nhưng được áp dụng muộn nhất. Cốt lõi là tính bất biến (immutability): biến được khởi tạo rồi không bao giờ đổi giá trị — nói cách khác, ngôn ngữ không có phép gán (no assignment).

Mọi race condition, deadlock và lỗi cập nhật đồng thời đều do biến có thể thay đổi (mutable variables). Không có biến nào bị cập nhật thì không thể có race condition; không có khóa thay đổi được thì không thể có deadlock.

Clean Architecture · Chương 6

Với kiến trúc sư quan tâm tới concurrency, đây là một kết luận mạnh: đẩy được code sang vùng bất biến tới đâu, vô hiệu hóa cả lớp lỗi đa luồng tới đó. Nhưng bất biến tuyệt đối đòi hỏi bộ nhớ và tốc độ vô hạn — nên trong thực tế cần các thỏa hiệp:

Tách biệt tính thay đổi

segregation of mutability

Chia ứng dụng thành thành phần bất biến (thuần chức năng) và thành phần cho phép thay đổi — phần mutable được bảo vệ bằng cơ chế như transactional memory. Lời khuyên: đẩy càng nhiều xử lý vào vùng bất biến càng tốt.

Event Sourcing

event sourcing

Không lưu trạng thái, chỉ lưu chuỗi giao dịch; cần trạng thái thì tính lại từ đầu. Không update, không delete ⟶ ứng dụng chỉ còn CR (không CRUD) ⟶ không thể có lỗi cập nhật đồng thời. Đây chính là cách hệ quản lý mã nguồn vận hành.

Immutability qua lăng kính RAG

RAG  Pipeline RAG chunk và embed hàng loạt tài liệu — môi trường lý tưởng để xử lý song song. Nếu chunk/embedding là bất biến, nhiều worker cùng chạy mà không có race condition hay concurrent update; không cần khóa, không lo deadlock:

Python · chunk/embedding bất biến → song song an toàn
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor

@dataclass(frozen=True)          # bất biến: tạo xong KHÔNG đổi
class Chunk:
    doc_id: str
    text: str

def embed(c: Chunk) -> tuple[Chunk, list[float]]:
    return c, model.encode(c.text)            # thuần chức năng, không gán biến chung

with ThreadPoolExecutor() as pool:
    vectors = list(pool.map(embed, chunks))   # nhiều worker — không race vì không có mutable state
Chunk bất biến và embed không chạm biến dùng chung, việc chạy song song không cần khóa — đúng tinh thần "không mutable thì không có lỗi đồng thời".

Ở ranh giới service, tầng web PHP (Laravel) chỉ gửi yêu cầu sang pipeline Python qua HTTP — nó không tự xử lý vòng đời chunk/embedding, nên không kéo trạng thái thay đổi vào ranh giới:

PHP · Laravel chỉ gọi sang service (ranh giới HTTP)
// Web/API chỉ chuyển tiếp; pipeline bất biến nằm bên Python
$res = Http::post('http://rag:8000/ingest', [
    'doc_id' => $doc->id,
    'text'   => $doc->content,   // gửi dữ liệu thô, không giữ trạng thái embed
]);
return response()->json($res->json());
PHP ở ranh giới service; toàn bộ pipeline bất biến (chunk→embed) sống trong Python. Đúng quy ước: Python lo nội-pipeline, PHP chỉ ở ranh giới.

05 Ghi nhớ nhanh

Paradigm là rào cản tích cực. Structured cấm goto, OO cấm con trỏ hàm trần, Functional cấm phép gán. Cả ba dạy ta điều không được làm — và không còn gì để cấm thêm.

Structured = chức năng chứng minh được. Mọi chương trình chỉ gồm tuần tự · lựa chọn · lặp; functional decomposition cho ta các đơn vị nhỏ, kiểm thử được. Test chứng minh code sai, không chứng minh đúng.

OO = quyền kiểm soát mọi phụ thuộc. Polymorphism cho phép đảo hướng phụ thuộc mã nguồn so với luồng điều khiển ⟶ plugin architecture, độc lập triển khai & phát triển.

Functional = an toàn đồng thời. Mọi race condition/deadlock/cập nhật đồng thời đều do mutable variables. Hãy đẩy càng nhiều code sang vùng bất biến càng tốt; segregation of mutability & event sourcing là cách thực tế.

Bản chất phần mềm không đổi từ 1946. Công cụ và phần cứng đổi chóng mặt, nhưng khối xây dựng vẫn chỉ là sequence · selection · iteration · indirection — nên các quy luật kiến trúc cũng bất biến.

NguồnChương 3–6 (Paradigm Overview, Structured/Object-Oriented/Functional Programming), Clean Architecture — Robert C. Martin, Prentice Hall 2017.