讓程式碼具有可讀性#

可讀性(readability)本質上是主觀的,但其核心目標是:讓其他工程師能夠快速且正確地理解程式碼在做什麼。要達成這個目標,往往需要具備同理心,試著從他人的角度思考哪些地方可能令人困惑或產生誤解。

本章涵蓋三大面向:

  • 讓程式碼能夠自我解釋的技巧
  • 確保程式碼中的細節對他人清晰可見
  • 正確使用語言特性的原則
mindmap
root((程式碼可讀性))
命名 Naming
註解 Comments
程式碼簡潔性 Conciseness
風格一致性 Style Consistency
巢狀深度 Nesting Depth
函式呼叫清晰度 Call Clarity
數值解釋 Magic Numbers
語言特性運用 Language Features

5.1 使用描述性名稱#

名稱不只是用來唯一識別事物,更是一份簡短摘要。就像「toaster」這個詞同時告訴你它是什麼、做什麼;如果改叫「object A」,就完全失去了這層資訊。程式碼中的類別、函式、變數命名也是同樣的道理。

非描述性名稱讓程式碼難以閱讀#

當命名毫無意義時(例如 Tpnssf),即使花 20-30 秒盯著程式碼看,也完全無法理解它在做什麼。

註解不是描述性名稱的替代品#

有人會透過大量註解來彌補糟糕的命名,但這會帶來問題:

  • 程式碼變得更雜亂,工程師需要同時維護註解和程式碼
  • 必須不斷上下捲動才能理解各個變數的意義
  • 在函式呼叫端看到 t.f(n) 時,仍然無法直接理解它的用途

解決方案:讓名稱具有描述性#

T 改為 Teampns 改為 playerNamesf() 改為 containsPlayer(),程式碼立刻變得容易理解:

  • 變數、函式和類別變成自我解釋的
  • 即使脫離上下文,像 team.containsPlayer(playerName) 這樣的呼叫也一目了然
  • 程式碼比使用註解更簡潔,維護負擔更低

5.2 適當使用註解#

註解可以服務不同目的:

  • 解釋程式碼做了什麼(what)
  • 解釋程式碼為什麼這樣做(why)
  • 提供使用說明等其他資訊

冗餘註解可能有害#

當程式碼已經足夠自我解釋時,重複描述「做了什麼」的註解是多餘的:

  • 增加維護負擔——修改程式碼時還得同步更新註解
  • 讓程式碼更雜亂——想像每一行都有一個說明註解

註解是不可讀程式碼的糟糕替代品#

如果需要靠註解來解釋程式碼做了什麼,真正的問題往往是程式碼本身不夠可讀。更好的做法是重構程式碼,例如用命名良好的 helper function 取代魔術般的陣列索引存取。

註解適合解釋「為什麼」#

程式碼不擅長自我解釋的是為什麼要這樣做。以下情境特別適合使用「why」註解:

  • 產品或商業決策的原因
  • 修復某個不直覺的 bug
  • 處理相依套件的奇怪行為

何時使用 why 註解:當程式碼的存在原因與某些背景知識有關,而其他工程師不一定知道這些背景時,「why」註解特別有價值。例如:「Legacy 使用者(v2.0 之前註冊)的 ID 是基於姓名產生的,詳見 issue #4218。」

高層級摘要註解很有用#

可以把註解想成書的概要:

  • 每一段都加上摘要 → 很煩、降低可讀性(如同低層級的 what 註解)
  • 書封底的簡介 → 非常有用,讓人快速判斷是否相關(如同類別層級的摘要文件)

適合撰寫高層級文件的場景:

  • 類別整體做什麼,以及需要注意的重要細節
  • 函式的輸入參數代表什麼
  • 函式的回傳值代表什麼

務必使用常識:本節的建議不是硬性規則。如果不得已需要包含位元運算等難以理解的邏輯,用 what 註解來解釋也完全合理。


5.3 不要執著於程式碼行數#

一般而言,程式碼行數越少越好——更少的行數意味著更少的維護負擔和更低的認知負荷。但有些工程師會把「最小化行數」推到極端,認為它比任何其他品質因素都重要。

行數只是一個代理指標(proxy measurement),我們真正在意的是:

  • 容易理解
  • 難以誤解
  • 難以意外破壞

不是所有的行都是平等的:一行極難理解的程式碼,其品質可能遠不如用 10 行(甚至 20 行)清楚表達的版本。

避免精簡但不可讀的程式碼#

以 parity bit 驗證為例,一行程式碼壓縮了大量的假設:

  • 最低 15 位元包含值、最高位元包含 parity bit
  • 各種 bitmask 的含義(0x7FFF0x8000
  • parity 的計算邏輯

這讓其他工程師不得不耗費大量時間才能理解,也使得假設不夠明確而容易被破壞。

解決方案:即使需要更多行數,也要讓程式碼可讀#

透過定義命名良好的 helper function 和常數(如 PARITY_BIT_INDEXextractEncodedParity()calculateParity()),程式碼變得更容易理解,子問題也變得可重用。

行數 vs 可讀性:保持行數精簡是好的原則,但確保程式碼可理解、健壯且不容易出錯更為重要。如果需要更多行數才能做到這些,那就值得。


5.4 遵循一致的程式碼風格#

就像書寫英文有文法規則和慣例一樣(例如 SaaS 而非 SAAS),程式碼也需要一致的風格慣例。

不一致的風格會造成混淆#

以常見慣例為例:類別名稱用 PascalCase、變數名稱用 camelCase。如果一個類別被命名為 connectionManager(小寫開頭),讀者很容易誤認為它是一個實例變數,而非一個類別。當程式碼寫成 connectionManager.terminateAll() 時,讀者可能以為這只會影響當前實例的連線,但實際上它是靜態方法,會終止所有連線——這就是一個因風格不一致而造成的嚴重 bug。

解決方案:採用並遵循風格指南#

  • 將類別命名為 ConnectionManager 而非 connectionManager,就能避免上述混淆
  • 風格指南通常涵蓋:命名慣例、語言特性的使用、縮排方式、套件結構、文件撰寫方式
  • 大多數團隊已有既定的風格指南,遵循即可
  • 若團隊尚無風格指南,可採用現成的(如 Google Style Guides

善用 Linter:Linter 是能自動偵測風格違規的工具,有些還能警告容易出錯的程式碼模式。雖然它們只能捕捉較簡單的問題,但作為快速檢查非常有用。


5.5 避免深層巢狀#

典型的程式碼由巢狀區塊組成:函式定義一個區塊、if 判斷式定義一個區塊、for 迴圈定義一個區塊。這些區塊層層嵌套時,可讀性會急速下降。

Figure 5.1: Control-flow logic (such as if-statements and for-loops) can often result in blocks of code that are nested within one another.

深層巢狀難以閱讀#

人眼不擅長追蹤每一行程式碼處於哪一層巢狀中。當多個 if-else 層層嵌套時:

  • 難以一眼看出各段邏輯在何種條件下執行
  • 難以判斷某些行程式碼是否可達(reachable)

解決方案:重構以減少巢狀#

當巢狀的每個分支都以 return 結束時,通常可以輕鬆地重新排列邏輯來消除巢狀——使用 early return 模式即可。例如:

// 巢狀版本
if (condition1) {
  return A;
} else {
  if (condition2) {
    return B;
  } else { ... }
}

// 扁平版本(early return)
if (condition1) { return A; }
if (condition2) { return B; }
...

巢狀通常是函式做太多事的徵兆#

當巢狀分支不以 return 結束時,通常意味著函式承擔了太多責任。例如一個函式同時包含「查找地址」和「寄送信件」的邏輯。

解決方案:拆分為更小的函式#

將子問題(如「查找車主地址」)提取到獨立函式中,就能:

  • 在子函式中輕鬆使用 early return 消除巢狀
  • 讓主函式更簡潔、更專注於高層邏輯

5.6 讓函式呼叫具有可讀性#

即使函式命名良好,如果呼叫時不清楚各引數的用途,可讀性仍然很差。例如 sendMessage("hello", 1, true) 中,1true 分別代表什麼?

過多參數的警訊:函式呼叫的可讀性會隨著引數數量增加而下降。過多的參數通常反映更根本的問題,例如抽象層次不當或模組化不足。

解決方案一:使用具名引數(Named Arguments)#

越來越多的現代語言支援具名引數:

sendMessage(message: "hello", priority: 1, allowRetry: true);

在 TypeScript 中,可以透過 object destructuring 達到類似效果:定義一個參數介面,函式接收一個物件並立即解構。

解決方案二:使用描述性型別#

用更具描述性的型別取代原始型別(primitive type):

  • 用 class 包裝值(如 MessagePriority
  • 用 enum 取代 Boolean(如 RetryPolicy.ALLOW_RETRY vs true

這樣即使不看函式定義,呼叫也一目了然:

sendMessage("hello", new MessagePriority(1), RetryPolicy.ALLOW_RETRY);

有時沒有完美解法#

當函式接收多個同型別的參數(如 BoundingBox(Int top, Int right, Int bottom, Int left)),如果語言不支援具名引數,最佳做法可能只是使用行內註解:

new BoundingBox(
    /* top= */ 10,
    /* right= */ 50,
    /* bottom= */ 20,
    /* left= */ 5);

但這依賴於註解正確且被持續維護。Builder pattern 是另一個選項,但會犧牲編譯期安全性。

Figure 5.2: Some IDEs augment the view of the code to make function calls more readable.

IDE 的輔助不能取代可讀性#

有些 IDE 會自動在呼叫處顯示參數名稱,但不能依賴這個功能——不是每個工程師都用同樣的 IDE,程式碼也常在沒有此功能的工具中被閱讀(如 code review 工具、merge 工具)。


5.7 避免使用未解釋的值#

硬編碼的值(hard-coded value)需要同時傳達兩件事:

  • 值是什麼(電腦執行需要)
  • 值代表什麼意思(工程師理解需要)

未解釋的值令人困惑#

以動能計算為例,程式碼中出現 907.18470.44704 這類數字(magic number),讀者無法得知它們分別是美噸轉公斤和英里/時轉公尺/秒的換算係數。當其他工程師需要修改程式碼時(例如改用公斤為單位),可能因不理解這些值而忘記移除,導致計算錯誤。

解決方案一:使用命名良好的常數#

private const Double KILOGRAMS_PER_US_TON = 907.1847;
private const Double METERS_PER_SECOND_PER_MPH = 0.44704;

在程式碼中使用常數名稱,讓意圖一目了然。

解決方案二:使用命名良好的函式#

有兩種方式:

  • Provider function:回傳常數值的函式(如 kilogramsPerUsTon()),概念上與常數類似
  • Helper function:執行轉換的函式(如 usTonsToKilograms(Double usTons)),將轉換視為子問題來解決,使用者不需要知道具體的換算係數

考慮重用性:如果其他工程師可能需要重用這些常數或 helper function,最好將它們放在公共的工具類別中,而非僅保留在當前使用的類別內。


5.8 適當使用匿名函式#

匿名函式(anonymous function)是沒有名稱的函式,通常在需要的地方以 inline 方式定義。

匿名函式適合小而自明的邏輯#

對於簡單、一目了然的操作,匿名函式非常簡潔有效:

allFeedback.filter(feedback -> !feedback.getComment().isEmpty());

匿名函式可能難以閱讀#

由於匿名函式沒有名稱,它不提供「這段邏輯在做什麼」的摘要。如果內容不夠自明(例如包含位元運算的 parity bit 檢查),讀者會很困惑。

解決方案:改用具名函式#

將複雜的匿名函式提取為具名函式(如 isParityBitCorrect),讓高層邏輯更清晰:

ids.filter(id -> id != 0)
   .filter(isParityBitCorrect);

讀者立刻明白:過濾掉零值、過濾掉 parity bit 不正確的 ID。如果想了解細節再去看具名函式即可。

大型匿名函式尤其有問題#

有些工程師把「函數式程式設計」等同於「使用 inline 匿名函式」,導致產出巨大且巢狀的匿名函式。Functional style 完全可以用具名函式來實現。

經驗法則:如果匿名函式超過 2-3 行,就應該考慮將它拆分為一個或多個具名函式。這不僅提升可讀性,也增加了可重用性。

解決方案:將大型匿名函式拆分為具名函式#

將巨大的 inline 匿名函式拆分為多個小型具名函式(如 buildFeedbackItem()buildTitle()buildCommentText()buildCategories()),可以:

  • 讓每個函式的職責一目了然
  • 讓高層邏輯清楚地表達 UI 顯示了什麼資訊
  • 各子函式可被獨立重用

5.9 審慎使用新語言特性#

工程師都喜歡「閃亮的新東西」,程式語言的新特性也不例外。語言設計者在加入新特性前會深思熟慮,很多場景下新特性確實能大幅提升程式碼品質。

新特性可以改善程式碼#

例如 Java 8 引入的 Stream API,讓過濾清單這類操作從冗長的 for 迴圈變成簡潔的鏈式呼叫,既更可讀也更不容易出錯。

冷門特性可能造成困惑#

即使某個特性有明確好處,也要考量團隊中其他工程師是否熟悉它。如果團隊只維護少量 Java 程式碼,且成員都不熟悉 Stream,那麼使用它帶來的改善可能比不上造成的困惑。

使用最適合工作的工具#

不要因為新特性能用就到處用。例如用 Stream 來從 Map 取值——這比直接呼叫 map.get(key) 既不可讀又低效。

避免為了新而新:使用語言特性的理由應該是「它是解決這個問題的最佳工具」,而不是「它很新很酷」。


5.10 本章摘要#

  • 不可讀的程式碼會導致:
    • 其他工程師浪費時間嘗試解讀
    • 誤解導致引入 bug
    • 當其他工程師需要修改時容易破壞程式碼
  • 提升可讀性有時會讓程式碼更冗長,但這通常是值得的取捨
  • 撰寫可讀程式碼需要同理心,設想他人可能覺得困惑之處
  • 真實場景各有不同的挑戰,撰寫可讀程式碼永遠需要常識與判斷力