01 Nhỏ! (Small!)
Quy tắc đầu tiên của hàm là chúng phải nhỏ. Quy tắc thứ hai là chúng phải nhỏ hơn thế nữa.
Robert C. Martin · Chương 3Hàm lý tưởng dài 2–4 dòng, hiếm khi quá 20 dòng. Mỗi hàm "trong suốt rõ ràng", kể một câu chuyện và dẫn bạn sang hàm kế theo một trật tự cuốn hút.
Khối lệnh & thụt lề (blocks & indenting): khối bên trong if/else/while nên chỉ một dòng — thường là một lời gọi hàm có tên mô tả. Nhờ vậy độ thụt lề của hàm không quá 1–2 cấp, hàm ngoài luôn nhỏ.
RAG Khâu nạp tài liệu (ingest) hay phình to vì vòng lặp lồng điều kiện. Kéo khối bên trong ra thành lời gọi hàm có tên:
def ingest(path):
for doc in load(path):
if doc.lang == "vi": # thụt lề 3–4 cấp
text = clean(doc.text)
for chunk in split(text, 800):
if len(chunk) > 50:
vec = embed(chunk)
store.add(vec, chunk)
def ingest(path):
for doc in load(path):
index_document(doc) # khối trong for = 1 lời gọi hàm
def index_document(doc):
if doc.lang != "vi":
return
for chunk in meaningful_chunks(doc):
store.add(embed(chunk), chunk)
Vì sao: khối lồng được rút thành hàm index_document có tên mô tả; thụt lề về 1–2 cấp; mỗi hàm đủ nhỏ để "đọc một hơi là hiểu".
02 Chỉ làm một việc
HÀM NÊN LÀM MỘT VIỆC. LÀM VIỆC ĐÓ THẬT TỐT. VÀ CHỈ LÀM VIỆC ĐÓ.
Lời khuyên kinh điển hơn 30 năm · Chương 3Khó ở chỗ "một việc" là gì? Định nghĩa của Martin rất thực dụng:
"Một việc" = một mức trừu tượng
Hàm làm một việc nếu mọi bước trong nó đều ở một mức trừu tượng ngay dưới tên hàm — mô tả được bằng một đoạn "ĐỂ … ta …".
Cách nhận biết làm nhiều việc
Nếu tách được một hàm khác mà tên nó không chỉ là diễn đạt lại phần cài đặt → hàm cũ đang làm hơn một việc. Hàm một-việc cũng không chia được thành các "section" (khai báo / khởi tạo / xử lý).
RAG Hàm answer dễ ôm đồm: vừa truy hồi, vừa ghép prompt cấp thấp, vừa gọi API, vừa log. Hạ nó về một orchestrator gồm các bước cùng mức:
def answer(q):
hits = store.search(embed(q), k=5) # cấp cao
prompt = "Context:\n"
for h in hits:
prompt += "- " + h.text + "\n" # cấp thấp: ghép chuỗi
prompt += "Q: " + q
resp = llm.chat(prompt) # cấp cao
log.info("answered %s", q); return resp.text # lẫn cả việc khác
def answer(q): # mọi bước cùng một mức
context = retrieve(q)
return generate(q, context)
def retrieve(q):
return store.search(embed(q), k=5)
def build_prompt(q, context): # chi tiết cấp thấp gói riêng
lines = [f"- {h.text}" for h in context]
return "Context:\n" + "\n".join(lines) + f"\nQ: {q}"
Vì sao: answer chỉ còn các bước cùng một mức ("ĐỂ trả lời: ta truy hồi rồi sinh"); việc ghép chuỗi cấp thấp đẩy vào build_prompt; bỏ phần log lạc đề khỏi hàm.
03 Một mức trừu tượng & Quy tắc Stepdown
Trộn các mức trừu tượng trong một hàm luôn gây nhầm lẫn — vừa gọi get_html() (cấp cao) vừa .append("\n") (cấp thấp). Mỗi hàm hãy giữ đúng một mức.
Quy tắc Stepdown (The Stepdown Rule): code nên đọc như một câu chuyện từ trên xuống — mỗi hàm được theo sau bởi các hàm ở mức trừu tượng thấp hơn kế tiếp, để ta đọc mà đi xuống từng cấp một.
Đọc như các đoạn "ĐỂ … ta …"
ĐỂ trả lời câu hỏi
…ta truy hồi ngữ cảnh, rồi sinh câu trả lời.
ĐỂ truy hồi ngữ cảnh
…ta nhúng câu hỏi, tìm các đoạn gần nhất, rồi lọc theo độ liên quan.
ĐỂ lọc theo độ liên quan
…ta so độ tương đồng cosine của mỗi đoạn với ngưỡng.
Mỗi cấp chỉ tham chiếu cấp ngay dưới — đó chính là cách giữ hàm nhỏ và "làm một việc". Học theo được Stepdown là khó, nhưng là chìa khóa.
04 Câu lệnh Switch & đa hình
Bản chất switch (và chuỗi if/elif) là luôn làm N việc. Một switch chọn-theo-loại còn vi phạm:
Vi phạm SRP
Có nhiều hơn một lý do để thay đổi.
Vi phạm OCP
Thêm một loại mới là phải sửa hàm — và sửa ở vô số hàm cùng cấu trúc.
RAG Quy tắc của Martin: switch chấp nhận được nếu chỉ xuất hiện một lần, dùng để tạo đối tượng đa hình, và bị giấu sau một factory. Ví dụ chọn nhà cung cấp embedding:
def embed(text, provider): # switch lặp khắp nơi
if provider == "openai": return openai_embed(text)
elif provider == "cohere": return cohere_embed(text)
elif provider == "local": return local_embed(text)
raise UnknownProvider(provider)
# … cùng switch này còn lặp ở rerank(), summarize(), …
class Embedder(Protocol):
def embed(self, text: str) -> list[float]: ...
def make_embedder(provider) -> Embedder: # switch CHỈ ở đây
return {"openai": OpenAIEmbedder,
"cohere": CohereEmbedder,
"local": LocalEmbedder}[provider]()
# nơi dùng: embedder.embed(text) — đa hình, không thấy switch
Vì sao: switch bị chôn trong factory để tạo đối tượng đa hình; mọi nơi gọi embedder.embed(...). Thêm nhà cung cấp mới = thêm một lớp, không sửa code đang gọi — tôn trọng SRP & OCP.
05 Đối số của hàm (Function Arguments)
Đối số "ngốn năng lực tư duy" và làm việc kiểm thử bùng nổ tổ hợp. Càng ít càng tốt:
| Số đối số | Tên gọi | Đánh giá |
|---|---|---|
| 0 | niladic | tốt nhất |
| 1 | monadic | tốt |
| 2 | dyadic | chấp nhận |
| 3 | triadic | tránh nếu được |
| > 3 | polyadic | cần lý do rất đặc biệt — vẫn nên tránh |
Tránh đối số cờ (flag argument)
Truyền một boolean vào hàm là tuyên bố oang oang rằng hàm làm hai việc — một việc nếu cờ True, việc khác nếu False. Hãy tách đôi:
def search(query, rerank): # cờ → search(q, True) khó hiểu
hits = vector_search(query)
if rerank:
hits = rerank_hits(query, hits)
return hits
def search(query):
return vector_search(query)
def search_reranked(query): # hai ý định gọi → hai hàm tên rõ
return rerank_hits(query, search(query))
Vì sao: search(q, True) buộc người đọc đoán cờ làm gì; tách thành search và search_reranked khiến mỗi hàm chỉ làm một việc, lời gọi tự kể ý định.
Đối số đối tượng & danh sách: khi cần > 2–3 đối số, thường vài cái nên gói thành một lớp — make_circle(Point center, radius) tốt hơn make_circle(x, y, radius). Còn các đối số xử lý đồng nhất (vd String.format(fmt, *args)) thì coi như một danh sách → vẫn là dyadic.
06 Không tác dụng phụ & Tách Lệnh–Truy vấn
Tác dụng phụ là những lời nói dối: hàm hứa làm một việc nhưng âm thầm làm việc khác.
Robert C. Martin · Chương 3Ví dụ kinh điển: checkPassword nghe như chỉ kiểm tra, nhưng lại lén gọi Session.initialize() — tạo ràng buộc thời gian (temporal coupling): chỉ gọi được "đúng lúc", gọi sai thứ tự là mất dữ liệu phiên.
RAG Một hàm "truy vấn" mà lén thay đổi trạng thái là cùng một cái bẫy:
def is_relevant(chunk, query): # tên: chỉ trả lời câu hỏi
score = cosine(embed(query), chunk.vector)
cache.clear() # tác dụng phụ ẩn → temporal coupling
return score >= 0.7
def is_relevant(chunk, query): # TRUY VẤN: chỉ trả lời, không đổi gì
return cosine(embed(query), chunk.vector) >= 0.7
def reset_cache(): # LỆNH: tách riêng, gọi tường minh khi cần
cache.clear()
Vì sao — Command Query Separation (CQS): hàm hoặc làm gì đó (lệnh) hoặc trả lời gì đó (truy vấn), không cả hai. Bỏ tác dụng phụ ẩn khiến is_relevant gọi lúc nào cũng an toàn.
07 Ưu tiên Exception thay mã lỗi
Trả về mã lỗi vi phạm CQS (lệnh bị dùng như biểu thức trong if) và đẩy người gọi vào các cấu trúc if lồng sâu để xử lý lỗi ngay. Dùng exception, rồi tách khối try/catch ra hàm riêng.
def delete_doc(doc): # mã lỗi → if lồng sâu
if store.delete(doc) == OK:
if registry.delete_ref(doc.name) == OK:
if keys.delete(doc.key) == OK:
return OK
return ERROR
def delete_doc(doc): # try là từ đầu tiên, hết sau except
try:
_delete_doc_and_refs(doc)
except StoreError as e:
log_error(e)
def _delete_doc_and_refs(doc): # xử lý thường, tách khỏi xử lý lỗi
store.delete(doc)
registry.delete_ref(doc.name)
keys.delete(doc.key)
Vì sao — Error Handling Is One Thing: xử lý lỗi cũng là "một việc", nên hàm có try thì try phải là từ đầu tiên và không gì sau except/finally. delete_doc chỉ lo lỗi; _delete_doc_and_refs chỉ lo xóa — đọc & sửa đều dễ.
08 DRY & cách thật sự để viết hàm
Đừng lặp lại chính mình (Don't Repeat Yourself): trùng lặp có thể là cội rễ của mọi cái ác trong phần mềm. Chuẩn hóa CSDL, OOP, lập trình cấu trúc/hướng khía cạnh (AOP)… rốt cuộc đều là chiến lược loại bỏ trùng lặp.
Không ai viết hàm sạch ngay lần đầu
Viết phần mềm cũng như viết văn: đổ ý ra trước, rồi gọt cho đến khi đọc xuôi. Martin thừa nhận bản nháp của ông cũng dài, lồng nhiều vòng lặp, tên tùy tiện, lắm đối số.
Viết nháp (rough draft)
Lộn xộn cũng được — miễn chạy đúng.
Phủ unit test
Có bộ test che mọi dòng vụng về đó.
Tinh chỉnh (refactor)
Tách hàm, đổi tên, bỏ trùng lặp, sắp lại — test luôn xanh — tới khi đạt mọi quy tắc trên.
Hàm là động từ, lớp là danh từ của ngôn ngữ đặc thù mà bạn thiết kế cho hệ thống. Nghệ thuật lập trình, xưa nay, là nghệ thuật thiết kế ngôn ngữ.
09 Ghi nhớ nhanh
Nhỏ & thụt lề ≤ 2 cấp. Khối trong if/while là một lời gọi hàm có tên.
Làm một việc = mọi bước ở một mức ngay dưới tên hàm; tách được hàm tên-không-phải-diễn-đạt-lại ⟹ đang làm nhiều việc.
Chôn switch bằng đa hình: một lần, trong factory, tạo đối tượng đa hình.
Ít đối số nhất có thể; tránh cờ boolean. Nhiều đối số → gói thành đối tượng.
Không tác dụng phụ; CQS — hoặc làm gì đó, hoặc trả lời gì đó.
Exception thay mã lỗi; xử lý lỗi là một việc — tách try/catch ra hàm riêng.
Viết nháp → test → refactor. Không ai viết hàm sạch ngay lần đầu.