01 Ẩn dụ xây thành phố
Một thành phố vận hành trơn tru dù không ai quản hết mọi thứ — không một cá nhân nào nắm toàn cảnh. Nó chạy được nhờ tách mối quan tâm (separation of concerns) thành các module & mức trừu tượng: đội điện, nước, giao thông mỗi đội lo phần của mình mà không cần hiểu toàn bộ.
Các chương trước lo code sạch ở mức thấp (hàm, lớp). Chương này nâng tầm: làm sao giữ sạch & tách bạch ở mức HỆ THỐNG — nơi các module ghép lại thành một kiến trúc lớn.
Tách theo mức trừu tượng
Mỗi mức chỉ hiểu phần việc của mình; tầng cao không vướng chi tiết tầng thấp — như người lái xe không cần biết nhà máy điện vận hành ra sao.
Separation of concerns
Đây là kỹ thuật cốt lõi để hệ phức tạp vẫn quản được — và là sợi chỉ xuyên suốt cả hai chương này.
02 Tách việc DỰNG khỏi việc DÙNG
Một hệ thống nên tách rõ quá trình khởi động (startup) — dựng đối tượng & nối dây phụ thuộc (wiring) — khỏi logic chạy lúc runtime. Đa số ứng dụng trộn lẫn hai thứ: code khởi tạo ad hoc rải khắp logic nghiệp vụ.
RAG Một thủ thuật quen thuộc là Lazy Initialization: chỉ dựng dịch vụ khi lần đầu cần. Nghe tiện, nhưng nó trộn việc dựng vào việc dùng:
class RagService:
_retriever = None
def answer(self, q):
if self._retriever is None: # dựng LẪN trong khi dùng
self._retriever = FaissRetriever( # hard-code lớp cụ thể
index="prod.idx", dim=384, model=OpenAIEmbedder())
return self._retriever.search(q) # … rồi mới dùng
class RagService:
def __init__(self, retriever): # đã được dựng & nối dây từ trước
self._retriever = retriever
def answer(self, q):
return self._retriever.search(q) # CHỈ dùng — không dựng gì
# việc dựng FaissRetriever(...) dồn hết về main / nơi lắp ráp
Vì sao: lazy init hard-code phụ thuộc FaissRetriever + mọi thứ constructor nó cần (không compile/khởi tạo nổi nếu chưa resolve, dù runtime có thể không dùng tới); lại khó test (phải gán test double / mock trước khi gọi, và phải kiểm cả nhánh None). Trộn dựng với xử lý thường = vi phạm SRP nhỏ. Đừng để một idiom tiện lợi phá vỡ tính module.
Tách riêng main (Separation of Main)
Dồn mọi khía cạnh dựng vào main (hay một module mà main gọi). Phần còn lại của hệ thống coi như mọi đối tượng đã được dựng & nối dây sẵn. main dựng đối tượng rồi trao cho application — application chỉ việc dùng.
Mọi mũi tên phụ thuộc qua ranh giới main ↔ application đều trỏ MỘT chiều — RA KHỎI main. Application không biết gì về main hay quá trình dựng. Khi application cần kiểm soát thời điểm tạo (vd tạo LineItem để thêm vào Order), dùng Abstract Factory: application nắm "khi nào", nhưng chi tiết dựng vẫn nằm phía main.
03 Dependency Injection & Inversion of Control
Dependency Injection (DI) là cơ chế mạnh để tách dựng khỏi dùng, áp dụng Inversion of Control (IoC): dời trách nhiệm thứ yếu (như tự đi tìm phụ thuộc) ra khỏi đối tượng, giao cho một đối tượng chuyên trách → mỗi lớp bớt việc, gần SRP hơn.
Đối tượng không tự instantiate phụ thuộc của nó; nó hoàn toàn thụ động, chỉ phơi ra constructor/setter để được tiêm (inject). Một container DI dựng đối tượng (thường on-demand) và nối dây phụ thuộc theo file cấu hình / module dựng. (JNDI lookup chỉ là DI "một phần" — đối tượng vẫn chủ động đi resolve.)
RAG Đối tượng tự new phụ thuộc của mình là tự trói vào lớp cụ thể; hãy tiêm chúng qua constructor:
class Pipeline:
def __init__(self):
self.embedder = OpenAIEmbedder(key="sk-...") # tự new → cứng
self.store = FaissStore("prod.idx") # khó thay/khó test
self.llm = OpenAIChat(model="gpt-4o")
class Pipeline:
def __init__(self, embedder, store, llm): # thụ động: nhận qua constructor
self.embedder = embedder
self.store = store
self.llm = llm
# container / main wire: Pipeline(OpenAIEmbedder(...), FaissStore(...), OpenAIChat(...))
# test: Pipeline(FakeEmbedder(), InMemoryStore(), StubLLM())
Vì sao: tự new bên trong khiến Pipeline phụ thuộc lớp cụ thể — không đổi được nhà cung cấp, không test nổi vì không thay được bằng fake. Tiêm phụ thuộc qua constructor làm đối tượng thụ động: main/container quyết định lắp gì, còn test lắp toàn double nhẹ.
04 Mở rộng dần & Cross-cutting concerns
Huyền thoại "làm đúng ngay từ đầu" là sai. Hệ thống phần mềm độc đáo ở chỗ kiến trúc của chúng có thể lớn dần — nếu ta duy trì việc tách mối quan tâm cho tốt.
Diễn giải ý Chương 11 · Robert C. MartinKhông cần Big Design Up Front (BDUF) — thiết kế đồ sộ trước khi viết dòng code nào. BDUF còn có hại vì cản trở thích nghi với thay đổi. Hãy bắt đầu "đơn giản ngây thơ" nhưng tách rời tốt, giao story chạy được nhanh, rồi thêm hạ tầng khi thực sự cần scale. (Phản ví dụ EJB1/EJB2: không tách concern tốt nên cản tăng trưởng hữu cơ.)
Mối quan tâm cắt ngang (cross-cutting concerns)
Vài mối quan tâm — persistence, security, transaction — không nằm gọn trong một đối tượng mà cắt NGANG nhiều đối tượng (vd muốn persist mọi đối tượng theo cùng một chiến lược). Dù framework & domain đều module hóa tốt, giao điểm mịn của chúng vẫn là vấn đề.
AOP (Aspect-Oriented Programming) khôi phục tính module cho các mối quan tâm cắt ngang: mỗi aspect chỉ định "ở những điểm nào cần chỉnh hành vi", một cách noninvasive (không phải sửa tay code đích). Java Proxies hợp cho wrap đơn giản nhưng rườm rà, khó sạch; các framework như Spring AOP / JBoss AOP dùng proxy nội bộ để bạn viết aspect bằng POJO thuần — POJO không phụ thuộc framework nên dễ test-drive.
05 Test-drive kiến trúc & trì hoãn quyết định
Nếu viết domain logic bằng POJO tách khỏi các mối quan tâm kiến trúc ở mức code, ta thật sự test-drive được kiến trúc — cho nó tiến hóa từ đơn giản đến tinh vi, thay vì khóa cứng từ đầu.
Kiến trúc tối ưu = các domain mối-quan-tâm được module hóa (POJO), tích hợp với nhau bằng các Aspect ít xâm lấn. Y như code, kiến trúc đó cũng nên test-drive được.
Phút chót có trách nhiệm (last responsible moment)
Module hóa + tách concern cho phép ra quyết định phi tập trung & trì hoãn. Hãy hoãn một quyết định tới "phút chót có trách nhiệm" — thời điểm muộn nhất mà vẫn kịp — để chọn dựa trên thông tin tốt nhất.
Quyết định sớm = quyết định với hiểu biết dưới mức tối ưu. Trì hoãn cho ta thêm dữ liệu khách hàng, thêm trải nghiệm, thêm lựa chọn công cụ. Và chỉ dùng chuẩn (standards) khi chúng thêm giá trị rõ ràng — đừng theo chuẩn vì giáo điều.
06 Bốn quy tắc thiết kế đơn giản
Chuyển sang Chương 12 — Emergence. Câu hỏi: thiết kế tốt có thể tự nổi lên không? Kent Beck nói có — nếu ta tuân theo bốn quy tắc thiết kế đơn giản, xếp theo thứ tự ưu tiên giảm dần.
1 · Chạy mọi test (Runs all the tests)
Trên hết, thiết kế phải tạo ra hệ thống chạy đúng ý định; hệ không kiểm chứng được thì không nên triển khai. Làm hệ testable đẩy ta tới lớp nhỏ, đơn trách nhiệm (SRP); coupling cao thì khó test → càng viết test càng phải dùng DIP/DI/interface để giảm coupling. Viết test dẫn tới thiết kế tốt hơn.
2 · Không trùng lặp (No duplication)
Trùng lặp là kẻ thù chính của thiết kế tốt: thêm việc, thêm rủi ro, thêm phức tạp thừa. Nó có nhiều dạng — dòng giống hệt; dòng tương tự (nắn cho giống rồi refactor); và trùng hiện thực.
3 · Diễn đạt ý định (Expresses intent)
Phần lớn chi phí dự án là bảo trì dài hạn; code phải nói rõ ý tác giả: đặt tên tốt; hàm & lớp nhỏ; dùng tên chuẩn của design pattern (COMMAND, VISITOR) để mô tả thiết kế ngắn gọn.
4 · Tối giản số lớp/method (Minimal classes & methods)
Ưu tiên thấp nhất. Số lớp/method phình to thường do giáo điều (ép tạo interface cho mọi lớp; tách cứng data class vs behavior class). Hãy thực dụng — nhưng đừng vì "ít" mà hi sinh ba quy tắc trên.
07 Refactoring — nơi áp ba quy tắc cuối
Có bộ test che chắn rồi, ta được tự do refactor: cứ vài dòng thêm vào lại dừng ngẫm "thiết kế có tệ đi không?"; nếu có thì dọn ngay rồi chạy test để chắc không hỏng gì. Chính bước refactor này là nơi áp ba quy tắc 2–4: bỏ trùng lặp, tăng diễn đạt, tối giản lớp/method (đồng thời tăng cohesion, giảm coupling, tách concern).
RAG Ví dụ kinh điển khử trùng hiện thực: đừng để is_empty() tự cài lại logic mà size() đã biết:
class ChunkBuffer:
def size(self):
return self._count # một cách đếm
def is_empty(self):
return self._count == 0 # lại tự đếm lần nữa → trùng
class ChunkBuffer:
def size(self):
return self._count
def is_empty(self):
return 0 == self.size() # một nguồn chân lý duy nhất
Vì sao: hai phương thức cùng "biết" cách đếm phần tử = trùng hiện thực; đổi cách lưu trữ là phải sửa cả hai. Cho is_empty() gọi lại size() dồn tri thức về một chỗ — đúng tinh thần quy tắc 2.
Template Method là kỹ thuật khử trùng lặp ở mức cao: khi hai thủ tục (vd accrueUSDivisionVacation / accrueEUDivisionVacation) chung một khung xương, đưa khung chung lên lớp cha, chỉ để lại các bước khác nhau cho lớp con — bỏ trùng cấu trúc mà vẫn diễn đạt rõ ý định.
08 Ghi nhớ nhanh
Sạch ở mức hệ thống = tách mối quan tâm theo các mức trừu tượng, đúng như cách một thành phố vận hành.
Tách DỰNG khỏi DÙNG. Dồn việc dựng vào main; lazy init trộn hai việc = SRP nhỏ bị vi phạm. Mũi tên phụ thuộc trỏ ra khỏi main.
DI / IoC: đối tượng thụ động, nhận phụ thuộc qua constructor/setter; đừng tự new phụ thuộc của mình.
Lớn dần, không BDUF. Cross-cutting concerns (persistence/security/transaction) → AOP / aspect noninvasive trên POJO. Trì hoãn quyết định tới phút chót có trách nhiệm.
Bốn quy tắc thiết kế đơn giản (theo ưu tiên): 1 Chạy mọi test · 2 Không trùng lặp · 3 Diễn đạt ý định · 4 Tối giản lớp/method.
Thiết kế tốt NỔI LÊN qua refactor: dưới lưới test, liên tục khử trùng lặp (vd is_empty() → 0 == size(); Template Method), làm code diễn đạt hơn, gọn hơn.