Clean Code
13 Phần IV · Test, Hệ thống & Đồng thời Chương 17

Mùi code & Kinh nghiệm

Smells and Heuristics

Chương cuối là một danh mục tra cứu: tổng hợp các "mùi code" (code smells) & heuristic mà Martin đúc kết khi viết lại các ví dụ trong sách. Mỗi mục có một mã hiệu (C·E·F·G·N·T) để bạn dùng làm checklist khi review — thấy mùi nào, ghi mã đó. Trang này gom theo nhóm cho dễ quét, kèm vài cặp Bẩn → Sạch cho các heuristic gặp nhiều nhất.

01 Catalog là gì & cách dùng

Đây không phải một danh sách quy tắc "học thuộc rồi áp máy móc", mà là một hệ thống giá trị (value system) về code sạch — đúc kết từ kinh nghiệm thực chiến. Mỗi mục được gắn mã hiệu theo nhóm để tham chiếu nhanh.

C · Comments

C1–C5

Mùi liên quan đến bình luận.

E · Environment

E1–E2

Build & chạy test.

F · Functions

F1–F4

Mùi của hàm.

G · General

G1–G36

Nhóm lớn nhất, đa dạng.

N · Names

N1–N7

Đặt tên.

T · Tests

T1–T9

Mùi của test.

Dùng khi review: đọc code (của mình hay người khác), thấy chỗ "ngứa mắt" thì đối chiếu danh mục và ghi mã hiệu vào nhận xét — vd "chỗ này G25 (magic number)", "hàm này F3 (flag argument)". Mã hiệu khiến phản hồi ngắn gọn, nhất quán, không cãi nhau cảm tính.

02 Comments (C1–C5)

Triết lý chung về comment: comment tốt nhất là comment không cần viết vì code đã tự nói. Năm mùi sau là các kiểu comment cần dọn:

Tên (EN)Ý chính
C1Inappropriate InformationĐừng để metadata (tác giả, lịch sử đổi, số ticket) trong comment — đó là việc của VCS / issue tracker.
C2Obsolete CommentComment cũ / sai / lạc đề: cập nhật hoặc xóa ngay, đừng để nó nói dối.
C3Redundant CommentĐừng comment thứ code đã tự nói (i++; // tăng i, Javadoc rỗng).
C4Poorly Written CommentĐã viết thì viết cho tốt: đúng ngữ pháp, súc tích, đáng đọc.
C5Commented-Out CodeXóa ngay code bị comment lại — VCS nhớ giùm, để lại chỉ gây hoang mang.

03 Environment (E) & Functions (F)

Environment — build & test phải "một thao tác"

Tên (EN)Ý chính
E1Build Requires More Than One StepBuild phải là một thao tác đơn giản — một lệnh / một nút, không phải chuỗi bước thủ công.
E2Tests Require More Than One StepChạy mọi test cũng chỉ bằng một lệnh / một nút.

Functions — mùi của hàm

Tên (EN)Ý chính
F1Too Many ArgumentsCàng ít đối số càng tốt (0, 1, 2); tránh từ 3 trở lên.
F2Output ArgumentsTránh đối số đầu ra; muốn đổi trạng thái thì đổi chính object sở hữu hàm.
F3Flag ArgumentsĐối số boolean = hàm làm nhiều việc → tách thành các hàm riêng.
F4Dead FunctionHàm không ai gọi → xóa đi.

04 General — phần 1 (G1–G18)

Nhóm General là lớn & đa dạng nhất. Phần đầu xoay quanh trùng lặp, mức trừu tượng, code chết & tính nhất quán:

Tên (EN)Ý chính
G1Multiple Languages in One Source FileHạn chế trộn nhiều ngôn ngữ trong một file.
G2Obvious Behavior Is UnimplementedHành vi mà ai cũng kỳ vọng (least astonishment) lại không có.
G3Incorrect Behavior at the BoundariesSai ở các trường hợp biên — đừng tin vào trực giác, hãy test biên.
G4Overridden SafetiesTắt cảnh báo / bỏ qua test thất bại là chuốc lấy rủi ro.
G5DuplicationDRY — mọi trùng lặp là một cơ hội trừu tượng hóa. Quy tắc tối quan trọng nhất nhóm này.
G6Code at Wrong Level of AbstractionTách rạch ròi khái niệm cấp cao khỏi chi tiết cấp thấp.
G7Base Classes Depending on Their DerivativesLớp cha không được phụ thuộc vào lớp con của nó.
G8Too Much InformationInterface hẹp, lộ ít — module tốt là module ít "mặt tiền".
G9Dead CodeCode không bao giờ chạy (if không thể đúng, catch không bao giờ ném) → xóa.
G10Vertical SeparationKhai báo biến / hàm gần nơi dùng.
G11InconsistencyLàm một kiểu thì làm nhất quán mọi nơi (principle of least surprise).
G12ClutterBỏ thứ thừa: hàm rỗng, biến không dùng, comment vô nghĩa.
G13Artificial CouplingĐừng buộc dính những thứ không liên quan tới nhau.
G14Feature EnvyMethod "thèm" dữ liệu / hàm của lớp khác hơn lớp của chính mình.
G15Selector ArgumentsĐối số chọn-hành-vi (cái false lủng lẳng) → tách thành nhiều hàm nhỏ.
G16Obscured IntentCode quá cô đọng / khó hiểu làm ý định bị che lấp.
G17Misplaced ResponsibilityĐặt mỗi thứ vào đúng nơi người đọc kỳ vọng nó thuộc về.
G18Inappropriate StaticƯu tiên non-static; chỉ static khi chắc chắn không bao giờ cần đa hình.

RAG  G5 (Duplication) là mùi đáng săn nhất. Trong pipeline RAG, đoạn "nhúng rồi chuẩn hóa vector" hay bị copy ở nhiều nơi — gom lại một chỗ:

Python · trước✗ Bẩn
def index(chunk):
    v = model.encode(chunk)
    v = v / norm(v)                 # chuẩn hóa — lặp ở đây
    store.add(v, chunk)

def search(query):
    v = model.encode(query)
    v = v / norm(v)                 # …và lặp y hệt ở đây
    return store.knn(v, k=5)
Python · sau✓ Sạch
def embed(text):                    # một nguồn chân lý duy nhất
    v = model.encode(text)
    return v / norm(v)

def index(chunk):  store.add(embed(chunk), chunk)
def search(query): return store.knn(embed(query), k=5)

Vì sao (G5 · DRY): logic "nhúng + chuẩn hóa" gom vào một hàm embed; đổi cách chuẩn hóa chỉ sửa một chỗ, và index/search không bao giờ lệch nhau.

05 General — phần 2 (G19–G36)

Phần sau của General thiên về diễn đạt ý định: biến giải thích, đa hình, đóng gói điều kiện, hằng có tên, luật Demeter…

Tên (EN)Ý chính
G19Use Explanatory VariablesChẻ tính toán phức tạp thành các biến trung gian có tên mang nghĩa.
G20Function Names Should Say What They Dodate.add(5) mơ hồ → addDaysTo / daysLater.
G21Understand the AlgorithmHiểu thật sự thuật toán, đừng "vọc tới khi qua test".
G22Make Logical Dependencies PhysicalModule phụ thuộc phải hỏi trực tiếp, không giả định (vd dùng getMaxPageSize() thay cho hằng giả định).
G23Prefer Polymorphism to If/Else or Switch/CaseCân nhắc đa hình trước khi viết switch theo loại (tôn trọng OCP).
G24Follow Standard ConventionsTheo quy ước chung của nhóm / ngôn ngữ.
G25Replace Magic Numbers with Named Constants86400SECONDS_PER_DAY; 55LINES_PER_PAGE.
G26Be PreciseChính xác: xử lý null, làm tròn, đồng thời… đừng đoán bừa.
G27Structure over ConventionƯu tiên ép buộc bằng cấu trúc (enum, type) hơn là quy ước "nhớ mà làm".
G28Encapsulate ConditionalsTrích boolean phức thành hàm tên rõ: if shouldBeDeleted(timer).
G29Avoid Negative Conditionalsif buffer.shouldCompact() dễ hơn if not buffer.shouldNotCompact().
G30Functions Should Do One ThingHàm chỉ làm một việc — chẻ nhỏ nếu thấy nó "và…".
G31Hidden Temporal CouplingsPhụ thuộc thứ tự gọi phải hiện rõ trong chữ ký hàm.
G32Don't Be ArbitraryCấu trúc code phải có lý do; đừng tùy tiện đặt đâu cũng được.
G33Encapsulate Boundary ConditionsGói điều kiện biên (vd level + 1) vào một biến / hàm, đừng rải khắp nơi.
G34Functions Should Descend Only One Level of AbstractionMỗi câu trong hàm thấp hơn tên hàm đúng một mức.
G35Keep Configurable Data at High LevelsHằng cấu hình đặt ở tầng cao, truyền xuống thay vì chôn ở tầng thấp.
G36Avoid Transitive NavigationLuật Demeter — "viết code nhút nhát"; tránh a.getB().getC().doSomething().

RAG  G25 (magic number) & G19 (explanatory variable) thường đi cùng nhau — số "thần kỳ" lẫn biểu thức rối trong một dòng lọc kết quả:

Python · trước✗ Bẩn
def keep(hits):
    return [h for h in hits
            if cosine(q_vec, h.vector) >= 0.78    # 0.78 là gì?
            and len(h.text) > 40]                  # 40 là gì?
Python · sau✓ Sạch
RELEVANCE_THRESHOLD = 0.78          # G25: hằng có tên
MIN_CHUNK_CHARS = 40

def keep(hits):
    def is_relevant(h):              # G19: biến/hàm giải thích
        similarity = cosine(q_vec, h.vector)
        long_enough = len(h.text) > MIN_CHUNK_CHARS
        return similarity >= RELEVANCE_THRESHOLD and long_enough
    return [h for h in hits if is_relevant(h)]

Vì sao (G25 + G19): 0.7840 giờ có tên nói rõ ý nghĩa; điều kiện rối được chẻ thành similarity / long_enough để dòng return đọc như tiếng Anh.

RAG  G23 (đa hình thay if/else) & G28 (đóng gói điều kiện): thay vì if theo loại reranker và một biểu thức boolean dài, dùng đa hình + một hàm tên rõ:

Python · trước✗ Bẩn
def rerank(kind, q, hits):
    if kind == "bm25":   return bm25_rerank(q, hits)
    elif kind == "cross": return cross_encoder(q, hits)   # G23: switch theo loại
    # đóng gói điều kiện?
    if len(hits) > 0 and hits[0].score < 0.3 and not cached(q):
        hits = expand(q, hits)
    return hits
Python · sau✓ Sạch
class Reranker(Protocol):           # G23: đa hình, mỗi loại một lớp
    def rerank(self, q, hits): ...

def needs_expansion(q, hits):       # G28: đóng gói điều kiện thành tên rõ
    return hits and hits[0].score < LOW_SCORE and not cached(q)

def rerank(reranker, q, hits):
    out = reranker.rerank(q, hits)
    return expand(q, out) if needs_expansion(q, out) else out

Vì sao (G23 + G28): chọn-theo-loại chuyển sang đa hình (thêm reranker mới = thêm một lớp, không sửa code đang gọi); biểu thức boolean rối được gói trong needs_expansion — đọc một dòng là hiểu.

RAG  G36 (Avoid Transitive Navigation — luật Demeter): đừng đi xuyên một chuỗi object để với tới thứ bạn cần.

Python · trước✗ Bẩn
def top_source(answer):
    # đi xuyên 4 cấp: biết quá nhiều về cấu trúc bên trong
    return answer.context.hits[0].document.metadata.source
Python · sau✓ Sạch
def top_source(answer):
    return answer.top_source()      # hỏi object, đừng moi ruột nó

# trong lớp Answer:
def top_source(self):
    return self.context.best_source()   # mỗi cấp chỉ nói với hàng xóm

Vì sao (G36 · Demeter): chuỗi a.getB().getC()… buộc bạn biết cấu trúc nội bộ của nhiều lớp — đổi một lớp là vỡ khắp nơi. "Code nhút nhát" chỉ nói chuyện với hàng xóm trực tiếp.

06 Names (N1–N7)

Tên là công cụ giao tiếp mạnh nhất trong code. Bảy heuristic về đặt tên:

Tên (EN)Ý chính
N1Choose Descriptive NamesTên rõ ràng, phản ánh đúng ý định — chọn kỹ, đổi không tiếc tay.
N2Choose Names at Appropriate Level of AbstractionTên theo khái niệm, không lộ chi tiết hiện thực.
N3Use Standard Nomenclature Where PossibleDùng thuật ngữ ngành / tên pattern (Decorator, Visitor) khi hợp.
N4Unambiguous NamesTên chỉ có một cách hiểu duy nhất.
N5Use Long Names for Long ScopesĐộ dài tên tỉ lệ với phạm vi: scope rộng → tên dài & rõ.
N6Avoid EncodingsKhông tiền tố kiểu m_, không Hungarian notation.
N7Names Should Describe Side-EffectsTên phản ánh mọi thứ hàm làm — đừng giấu hành vi ngầm.

07 Tests (T1–T9)

Một bộ test sạch & đầy đủ là tấm lưới an toàn cho mọi refactor. Chín heuristic về test:

Tên (EN)Ý chính
T1Insufficient TestsTest mọi thứ có thể hỏng, không chỉ "đủ dùng cho qua".
T2Use a Coverage Tool!Công cụ coverage lộ ra những vùng chưa được test.
T3Don't Skip Trivial TestsTest nhỏ dễ viết, giá trị tài liệu cao — đừng bỏ qua.
T4An Ignored Test Is a Question about an AmbiguityTest bị @Ignore là một câu hỏi đang treo về điều mơ hồ.
T5Test Boundary ConditionsChú ý đặc biệt các điều kiện biên.
T6Exhaustively Test Near BugsLỗi đi theo nhóm; thấy một lỗi thì test thật kỹ quanh đó.
T7Patterns of Failure Are RevealingMẫu hình thất bại của các test hé lộ nguyên nhân gốc.
T8Test Coverage Patterns Can Be RevealingNhìn chỗ nào được / chưa được chạy qua cũng để lộ vấn đề.
T9Tests Should Be FastTest chậm rồi sẽ bị bỏ chạy — giữ cho nhanh.

08 Ghi nhớ nhanh

Catalog = checklist review. Thấy mùi nào ghi mã đó (C·E·F·G·N·T) — phản hồi ngắn gọn & nhất quán.

G5 (DRY) là mùi quan trọng nhất: mọi trùng lặp là một cơ hội trừu tượng hóa.

G25 + G19: magic number → hằng có tên; biểu thức rối → biến giải thích.

G23 + G28: đa hình thay switch theo loại; đóng gói điều kiện boolean phức thành hàm tên rõ.

G36 (Demeter): hỏi object, đừng moi ruột nó — tránh a.getB().getC()….

Comments: comment tốt nhất là cái không cần viết — xóa C1/C3/C5, sửa C2/C4.

Tests: đủ & nhanh — test mọi thứ có thể hỏng (T1), dùng coverage (T2), giữ nhanh (T9).

NguồnChương 17 (Smells and Heuristics), Clean Code: A Handbook of Agile Software Craftsmanship — Robert C. Martin, Prentice Hall 2008.