為什麼要讓壞程式碼更顯眼#
有時候我們明知程式碼不夠好,卻因為時間或複雜度的限制無法立即重構。這時很多人會做一點小修飾「讓它不那麼糟」,但作者認為這是個錯誤——與其把問題藏起來,不如讓壞程式碼醜到無法忽視。這個過程稱為 anti-refactoring。
留下明顯的壞程式碼有兩個好處:
- 容易再次找到:開發者專注解決問題時,不顯眼的東西會被忽略,但明顯醜陋的程式碼不會
- 發出信號:告訴團隊和主管目前的約束條件(時間、優先級)不可持續
交付明顯壞的程式碼來發出信號,需要團隊具備足夠的心理安全感(psychological safety)——信任「傳訊者不會被射殺」。Google 的 Project Aristotle 研究顯示,心理安全感是團隊生產力最重要的因素。
將程式碼分為 Pristine 與 Legacy#
作者主張將程式碼二分為原始碼(pristine code) 與遺留碼(legacy code)。如果沒有時間或能力把程式碼提升到「相當好」的水準,就應該讓它降到「明顯差」——不要留在不上不下的「夠用」灰色地帶。
當能一眼辨識出哪些程式碼是 pristine、哪些是 legacy,就能估算每個檔案的好壞比例,有效地引導重構精力。作者建議優先重構那些最接近全面 pristine 的檔案,理由有二:
- 周圍的程式碼已經夠好,重構時不容易掉進兔子洞
- 破窗理論(Broken Window Theory):如果整個檔案都是乾淨的,人們會傾向維持乾淨;但只要看到一段壞程式碼,就更容易在旁邊堆上更多壞程式碼
定義「壞程式碼」的幾種方式#
沒有完美的方法判斷程式碼的好壞,但有幾種流行的估算方式:
本書的規則:簡單且具體#
Part 1 引入的規則設計為即使注意力分散時也能一眼辨識。缺點是這些規則不具普遍性——沒讀過本書的團隊成員不一定認同這些信號。
Code Smells:完整且抽象#
來自 Martin Fowler 的 Refactoring 與 Robert C. Martin 的 Clean Code 等經典著作。大部分 code smells 需要相當的練習才能一眼識別,但有些簡單到可以在入門課程中教授,例如 Magic constants 和 Duplicated code。
Cyclomatic Complexity:演算法化(客觀)#
計算程式碼中的路徑數量。每個 if、for、while 各增加一條路徑;在表達式層級,每個 || 和 && 也各分出一條路徑。這個指標同時給出測試數量的下限。人類在目測估算時,通常依賴縮排深度。
Cognitive Complexity:演算法化(主觀)#
較新的指標,估算人類閱讀程式碼時需要同時記住多少資訊。與 cyclomatic complexity 相比,它對巢狀結構的懲罰更重。但對於人眼一瞥能捕捉到的特徵,同樣歸結為縮排深度。
安全破壞的三條規則#
在「破壞」(vandalize)程式碼——也就是讓壞程式碼更顯眼——時,必須遵守三條規則:
| # | 規則 | 說明 |
|---|---|---|
| 1 | 絕不摧毀正確的資訊 | 例如方法名稱是好的但方法體很亂,不應為了讓方法更顯眼而改壞名稱。可以移除不正確或多餘的資訊(如過期的註解) |
| 2 | 不讓未來的重構更困難 | 應盡量讓下一個重構者更輕鬆,例如在適當位置放空白行提示 Extract method 的切割點 |
| 3 | 結果必須一眼可見 | 確保壞程式碼與 pristine 程式碼之間有明顯的視覺落差 |
遵守這三條規則,最壞的情況也只是改動被輕鬆還原。這保證了 anti-refactoring 不會造成永久傷害。
十種讓壞程式碼顯眼的具體方法#
使用 Enum#
把 Boolean 或 type code 替換為 enum。Enum 容易一眼辨識、比 Boolean 更具可讀性(因為有名字),而且有標準的重構路徑:Replace type code with classes → Push code into classes → Try delete then compile。
使用 Int 或 String 作為 Type Code#
在快速實驗階段,用 string 或命名常數的 int 作為 type code。String 的好處是文字本身就是名稱,不需事先宣告所有值。等實驗穩定後,下一步自然是將其轉為 enum,形成級聯式改善。
放入 Magic Number#
如果常數命名不當或不正確,不妨直接 inline 為 magic number。幾乎所有開發者都會對程式碼中的 magic number 產生反應,這是極有效的聚光燈。如果有任何有用的資訊,在 inline 時以註解保留。
加入註解#
在適當位置加入描述性註解——特別是那些可以成為方法名稱的註解。在遵循「少用註解」的團隊中,註解本身就是顯眼的信號,同時為未來重構提供切入點與方法名稱建議。
加入空白行#
用空白行分隔邏輯群組,暗示 Extract method 或 Encapsulate data 的切割點。當還不確定該如何命名分組時,空白行是比註解更低承諾的替代方案。
依命名分組#
將具有共同詞綴(common affixes) 的欄位或方法排列在一起,讓 Never have common affixes 規則的違反更加醒目,直接指向 Encapsulate data 的重構方向。
為名稱加入上下文#
如果方法或欄位尚未共享共同詞綴,可以在名稱中加入上下文後綴。甚至可以用底線(如 avg_ArrUtil)打破慣用的 camelCase 風格,讓信號更刺眼。
製造長方法#
如果發現某些方法被不恰當地拆分,反而隱藏了真正的結構,不妨將它們 inline 回一個長方法。長方法是大多數開發者的警報信號,雖然即時辨識度不如其他方法,但開發者會記住長方法的位置。Inline 時用註解保留原方法名稱的資訊。
給方法大量參數#
讓方法擁有大量參數——這不僅在定義處顯眼,在每個呼叫處也都是「需要重構」的路標。如果看到有人用 HashMap 或 data object 來規避大量參數,應將其還原為顯式參數。
使用 Getter/Setter#
將全域變數或 public 欄位封裝進類別,透過 getter/setter 存取。get 和 set 前綴在定義處和呼叫處都很顯眼,隨著程式碼推入類別中(push code into classes),這些 getter/setter 會自然消失。
mindmap
root((讓壞程式碼顯眼))
型別信號
使用 Enum
Int/String Type Code
Magic Number
結構信號
加入註解
加入空白行
製造長方法
命名信號
依命名分組
加入上下文後綴
介面信號
大量參數
Getter/Setter本章重點#
- 可以用壞程式碼來發出流程問題的信號(時間不足、優先級不當)
- 將程式碼分為 pristine 與 legacy,pristine 程式碼傾向維持更久的乾淨狀態
- 定義壞程式碼的方式有多種:本書規則、code smells、cyclomatic complexity、cognitive complexity
- 遵守三條規則就能安全地拉大 pristine 與 legacy 之間的差距:不摧毀資訊、不妨礙重構、讓問題一目了然
- 十種具體做法涵蓋從 enum 到 getter/setter,每一種都安全、可逆、且為未來重構鋪路