為什麼要讓壞程式碼更顯眼#

有時候我們明知程式碼不夠好,卻因為時間或複雜度的限制無法立即重構。這時很多人會做一點小修飾「讓它不那麼糟」,但作者認為這是個錯誤——與其把問題藏起來,不如讓壞程式碼醜到無法忽視。這個過程稱為 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 constantsDuplicated code

Cyclomatic Complexity:演算法化(客觀)#

計算程式碼中的路徑數量。每個 ifforwhile 各增加一條路徑;在表達式層級,每個 ||&& 也各分出一條路徑。這個指標同時給出測試數量的下限。人類在目測估算時,通常依賴縮排深度

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 存取。getset 前綴在定義處和呼叫處都很顯眼,隨著程式碼推入類別中(push code into classes),這些 getter/setter 會自然消失。

mindmap
  root((讓壞程式碼顯眼))
    型別信號
      使用 Enum
      Int/String Type Code
      Magic Number
    結構信號
      加入註解
      加入空白行
      製造長方法
    命名信號
      依命名分組
      加入上下文後綴
    介面信號
      大量參數
      Getter/Setter

本章重點#

  • 可以用壞程式碼來發出流程問題的信號(時間不足、優先級不當)
  • 將程式碼分為 pristinelegacy,pristine 程式碼傾向維持更久的乾淨狀態
  • 定義壞程式碼的方式有多種:本書規則、code smells、cyclomatic complexity、cognitive complexity
  • 遵守三條規則就能安全地拉大 pristine 與 legacy 之間的差距:不摧毀資訊、不妨礙重構、讓問題一目了然
  • 十種具體做法涵蓋從 enum 到 getter/setter,每一種都安全、可逆、且為未來重構鋪路