01 Code sạch là một hành trình
Để viết được code sạch, trước hết bạn phải viết code bẩn rồi dọn nó.
Robert C. Martin · Chương 14Không ai gõ ra code sạch ngay từ bản nháp đầu. Lập trình là một nghề thủ công (craft) hơn là một khoa học: bạn đổ ý tưởng ra để nó chạy được, rồi mới gọt giũa không ngừng cho tới khi nó đẹp. Sạch là điểm đến của một quá trình tinh luyện tăng tiến (successive refinement), không phải điểm khởi hành.
Test là điểm tựa của refactor: trong cả ba case, Martin chỉ dám "xé nát rồi ráp lại" vì có một bộ test (unit test, có khi thêm acceptance test) giữ hệ thống luôn "xanh" sau mỗi thay đổi nhỏ. Không có lưới an toàn đó, dọn code chỉ là canh bạc.
"Chạy được" là chưa đủ. Dừng lại ngay khi code vừa chạy là thiếu chuyên nghiệp — bởi bad code rots and ferments (code tồi mục rữa và lên men), kéo tốc độ cả đội đi xuống. Trách nhiệm của ta là để lại module sạch hơn lúc nhận.
02 Case 1 — Args parser (Successive Refinement)
Args là bộ phân tích tham số dòng lệnh. Khi chỉ xử lý cờ Boolean, code rất sạch. Nhưng vừa thêm kiểu String rồi Integer, nó biến thành cái mà Martin gọi là festering pile — "đống đang rữa": số instance variable phình ra, lẫn chuỗi "TILT" với HashSet/TreeSet, và các khối try-catch-catch chồng lên nhau. Mỗi kiểu đối số mới buộc phải sửa ở ba chỗ: chọn map khi parse schema, chuyển kiểu khi parse dòng lệnh, và một hàm getXXX trả về kiểu thật.
Phản xạ đúng của Martin: DỪNG thêm tính năng, quay lại refactor. Có sẵn bộ unit test (JUnit) + acceptance test (FitNesse) làm điểm tựa, ông thực hiện rất nhiều thay đổi nhỏ — mỗi cái dịch dần cấu trúc về một khái niệm chung ArgumentMarshaler — và giữ test luôn xanh sau từng bước.
class Args:
def __init__(self, schema, args):
self.bool_args = {} # mỗi kiểu thêm vào → một map mới
self.string_args = {}
self.int_args = {}
# … instance var cứ phình ra theo số kiểu
def parse_element(self, c, tail):
if tail == "": # cờ boolean
self.bool_args[c] = False
elif tail == "*": # string
self.string_args[c] = ""
elif tail == "#": # integer
self.int_args[c] = 0
# thêm kiểu mới = thêm một nhánh if ở ĐÂY…
def set_arg(self, c):
if c in self.bool_args: # … và lặp lại chuỗi if ở ĐÂY
self.bool_args[c] = True
elif c in self.string_args:
try: self.string_args[c] = self.next_arg()
except StopIteration: ... # try-catch chồng lên nhau
elif c in self.int_args:
try: self.int_args[c] = int(self.next_arg())
except ValueError: ...
except StopIteration: ...
class ArgumentMarshaler(Protocol): # một khái niệm chung
def set(self, it): ...
def get(self): ...
class BooleanMarshaler:
def set(self, it): self.value = True
def get(self): return self.value
class StringMarshaler:
def set(self, it): self.value = next(it)
def get(self): return self.value
class IntegerMarshaler:
def set(self, it): self.value = int(next(it))
def get(self): return self.value
class Args:
def set_arg(self, c): # KHÔNG còn type-case
self.marshalers[c].set(self.current)
# thêm kiểu mới = thêm MỘT marshaler, Args không đổi
Vì sao: mỗi kiểu đối số gói vào một ArgumentMarshaler đa hình có set()/get() riêng. Chuỗi if chọn-theo-kiểu biến mất; set_arg chỉ lấy marshaler theo ký tự rồi gọi m.set(...). Thêm kiểu mới giờ là thêm một lớp, không phải sửa Args ở ba chỗ.
Cốt lõi của Successive Refinement: không có cú "viết lại từ đầu" nào cả. Chỉ là một chuỗi dài những bước nhỏ an toàn, mỗi bước giữ test xanh, dần dịch một mớ rữa thành một thiết kế đa hình gọn gàng.
03 Case 2 — ComparisonCompactor của JUnit (Boy Scout)
ComparisonCompactor là module nhỏ trong JUnit: khi hai chuỗi khác nhau (vd ABCDE vs ABXDE) nó hiển thị gọn phần khác biệt thành <...B[X]D...>. Điểm đặc biệt của case này: code gốc đã tốt — "phân vùng đẹp, diễn đạt hợp lý, đơn giản". Vậy mà vẫn cải thiện được, theo Boy Scout Rule: hãy để lại chỗ cắm trại sạch hơn lúc bạn đến.
class ComparisonCompactor:
def __init__(self, ctx_len, expected, actual):
self.fCtxLen = ctx_len # N6: tiền tố f thừa
self.fExpected = expected
self.fActual = actual
self.fPrefix = 0 # thật ra là chỉ số (index)
self.fSuffix = 0
def compact(self, message): # N7: tên giấu side-effect
if self.fExpected is None or self.fActual is None \
or self.fExpected == self.fActual: # G28: điều kiện trần
return Assert.format(message, self.fExpected, self.fActual)
self.find_common_prefix()
self.find_common_suffix() # G31: lén phụ thuộc fPrefix
...
class ComparisonCompactor:
def __init__(self, ctx_len, expected, actual):
self.context_length = ctx_len # N6: bỏ tiền tố f
self.expected = expected
self.actual = actual
self.prefix_index = 0 # đổi tên: đúng là index
self.suffix_index = 0
def format_compacted_comparison(self, message): # N7: tên thật thà
if self.can_be_compacted(): # G28: đóng gói điều kiện
return self.compact_expected_and_actual(message)
return Assert.format(message, self.expected, self.actual)
def can_be_compacted(self):
return (self.expected is not None and self.actual is not None
and self.expected != self.actual)
def find_common_suffix(self, prefix_index): # G31: phơi phụ thuộc
... # truyền vào, không lén
Vì sao: bỏ tiền tố f của member (N6 — môi trường nay khỏi cần mã hóa scope); đóng gói điều kiện đầu hàm thành can_be_compacted() (G28); đổi compact → format_compacted_comparison vì tên cũ giấu việc "có thể không nén, mà trả message đã format" (N7); và phơi ràng buộc thời gian ẩn — find_common_suffix vốn lén dựa vào prefix_index do find_common_prefix tính trước — bằng cách truyền prefix_index làm tham số (G31).
Hai bài học: (1) không module nào miễn nhiễm cải thiện — dù đã tốt, mỗi người vẫn có trách nhiệm để nó sạch hơn. (2) Refactor là một quá trình lặp đầy thử–sai: có lúc Martin đảo lại quyết định trước đó (gộp lại một method vừa tách, đổi nghĩa một biểu thức) rồi mới hội tụ dần tới thứ "xứng đáng chuyên nghiệp".
04 Case 3 — SerialDate (Make it work → make it right)
SerialDate (gói org.jfree.date, tác giả David Gilbert) là một lớp ngày tháng "good code" thật sự. Martin "xé nó ra từng mảnh" — nhưng như một cuộc phê bình chuyên nghiệp, không ác ý: bác sĩ, phi công, luật sư đều bị review công khai để học hỏi; lập trình viên cũng nên thế.
First make it work, then make it right.
Robert C. Martin · Chương 16Bước 1 — Make It Work: dựng lưới test trước
Test có sẵn pass nhưng không phủ hết (T1): có hàm chết monthCodeToQuarter không ai gọi (F4), và Clover báo coverage chỉ 91/185 câu lệnh (~50%, T2). Martin viết một bộ test độc lập, toàn diện, kéo coverage lên ~92% (170/185); nhiều test ghi lại hành vi mong muốn, và quá trình đó lộ vài lỗi biên (T5/T6/T7) — sửa bằng cách ném IllegalArgumentException.
Bước 2 — Make It Right: dọn từ trên xuống
Có lưới test rồi, Martin áp checklist mùi code (Chương 17), chạy mọi test JCommon sau mỗi thay đổi.
class MonthConstants:
JANUARY = 1 # G25: hằng "trần", chỉ là int
FEBRUARY = 2
# …
def is_valid_month_code(code): # phải tự kiểm tra vì month là int
return 1 <= code <= 12
class SerialDate:
@staticmethod
def add_days(days, base): # G18: static, không đa hình được
serial = base.to_serial() + days
return SpreadsheetDate(serial)
def add_months(self, months):
# tính toán dài dòng, không biến giải thích → khó đọc
yy = (12 * self.get_yyyy() + self.get_month() - 1 + months) // 12
...
class Month(Enum): # G25: int → enum, tự hợp lệ
JANUARY = 1
FEBRUARY = 2
# … (không cần is_valid_month_code nữa)
@staticmethod
def from_int(n): return Month(n) # N1: make → from_int
def to_int(self): return self.value
class DayDate: # base KHÔNG biết derivative
def add_days(self, days): # G18: instance method
ordinal = self.to_ordinal() + days # N1: toSerial → toOrdinal
return DayDateFactory.make_date(ordinal)
def add_months(self, months):
total = self.month.to_int() - 1 + months # G19: biến giải thích
new_year = self.year + total // 12
new_month = Month.from_int(total % 12 + 1)
...
Vì sao: MonthConstants (int) → enum Month nên không còn cần is_valid_month_code + nhánh báo lỗi month code (G5/G27); magic 1 → Month.JANUARY.to_int() (G25); add_days static → instance method để có thể đa hình (G18); và dùng explaining temporary variables để các phép tính ngày tháng đọc được (G19).
Abstract Factory để cắt phụ thuộc ngược: base class DayDate vốn biết lớp con cụ thể của nó (G7 — base phụ thuộc derivative, một mùi nặng). Martin tách ra một DayDateFactory lo việc tạo instance và trả lời ngày min/max — base trở lại "vô tri" về lớp con. Cùng với đổi tên getYYYY → getYear (N1), kết quả: coverage tăng, vài bug được sửa, code rõ & thu gọn hơn — đúng tinh thần Boy Scout.
05 Bài học chung
Ba case khác nhau về xuất phát điểm (mớ rữa · code tốt · code rất tốt) nhưng hội tụ về cùng ba chân lý:
Sạch là một hành trình
Không ai viết sạch từ bản nháp đầu. Code sạch là kết quả của dọn dẹp & tinh chỉnh không ngừng — viết bẩn rồi dọn, làm nó chạy rồi làm nó đúng.
Test là bảo hiểm
Refactor chỉ an toàn khi có một bộ unit test mạnh làm điểm tựa. Trong cả ba case, mỗi thay đổi nhỏ đều được một bộ test xác nhận trước khi đi tiếp.
Sự tỉ mỉ tạo đẳng cấp
Từ đổi một tên biến tới tách một hàm — cái nhỏ vẫn đáng làm. Code sạch là code được chăm sóc; phê bình code chuyên nghiệp là cách ta cùng học.
"Honesty in small things is not a small thing." Sự trung thực trong những điều nhỏ không phải là điều nhỏ. Một tên biến thành thật, một hàm chỉ làm một việc, một phụ thuộc thứ tự được phơi rõ — cộng dồn lại chính là sự khác biệt giữa thợ và nghệ nhân.
06 Ghi nhớ nhanh
Viết bẩn rồi dọn. Code sạch là điểm đến của tinh luyện tăng tiến, không phải điểm khởi hành. "Chạy được" chưa đủ.
Case 1 · Args: mớ rữa của type-case & try-catch chồng → DỪNG thêm tính năng, refactor từng bước nhỏ giữ test xanh → đa hình ArgumentMarshaler, bỏ chọn-theo-kiểu.
Case 2 · ComparisonCompactor: code đã tốt vẫn dọn được (Boy Scout) — bỏ tiền tố f (N6), đóng gói điều kiện (G28), tên thật thà (N7), phơi temporal coupling (G31). Refactor là lặp, có khi đảo lại.
Case 3 · SerialDate: viết test toàn diện (~50% → ~92%) trước, rồi dọn — int → enum Month, magic 1 → hằng có tên (G25), static → instance (G18), biến giải thích (G19), Abstract Factory để base không biết derivative (G7).
Test là điểm tựa của mọi refactor; sự tỉ mỉ tạo đẳng cấp — "honesty in small things is not a small thing".