核心觀點#
作者 Robert C. Martin 列出一系列具有「壞味道(Bad Smells)」的程式碼特徵。
當你嗅到這些味道時,通常代表該處隱藏著設計問題,值得我們停下來重構。
這份列表真正的貢獻,並非在於規則,而在於提出一個價值體系。
軟體工藝(Software Craftsmanship)的專業性,正來自於對這些價值觀的堅持。
註解 (Comments)#
註解是雙面刃,壞的註解比沒有註解更糟。
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| C1 | 包含不妥資訊 | 註解應只保留技術性(程式設計或架構面)的內容。 像修改歷史、JIRA 單號等日誌資訊應交給 Git 處理 |
| C2 | 廢棄的註解 | 過時、不相關或不正確的註解會誤導讀者, 應立即更新或刪除 |
| C3 | 多餘的註解 | 如果程式碼本身已夠清楚(Self-documenting), 就不需要再寫註解重述一遍 |
| C4 | 拙劣的註解 | 如果必須寫註解,請字斟句酌,確保文法正確且詞意精準, 別像在寫隨手便條 |
C5: 被註解掉的程式碼 (Commented-Out Code)
這是最常見的壞味道,請直接刪除,不要擔心遺失。
版本控制系統(Git)會幫你記得它。
開發環境 (Environment)#
開發環境的設置應當極簡化,以減少人為錯誤。
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| E1 | 建立只需單一步驟 | 系統的建置(Build)不應包含從倉庫簽出程式碼以外的複雜指令。 應能一鍵完成 |
| E2 | 測試只需單一步驟 | 執行所有單元測試應該只需要一個按鈕或一個指令 |
函式 (Functions)#
函式設計的核心在於「專注」與「簡潔」。
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| F1 | 參數過多 | 參數量應由少至多排序:0 < 1 < 2 « 3。 超過三個通常代表設計有問題 |
| F2 | 輸出型參數 | 讀者習慣將參數視為輸入,避免將參數用作輸出(回傳值) |
| F3 | 旗標參數 (Flag Arguments) | 傳入布林值通常意味著函式做了「兩件事」 (True 做一件事,False 做另一件事),違反單一職責原則 |
| F4 | 未被呼叫的函式 | 也就是死函式(Dead Function),請勇敢刪除它 |
一般性問題 (General)#
這是最龐大的一類,涵蓋了設計原則與實作細節。
抽象與依賴#
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| G6 | 錯誤的抽象層次 | 高層次概念應放在基底類別, 低層次實作細節應放在衍生類別 |
| G7 | 基底相依於衍生類別 | 基底類別不該知道關於衍生類別的任何資訊 |
| G14 | 依戀情節 (Feature Envy) | 當一個類別方法對「另個類別」的變數與函式感興趣的程度, 超過對自己類別的興趣時,代表該方法可能放錯位置了 |
| G22 | 邏輯相依 vs 實體相依 | 相依模組不該對被相依模組有任何 「預先假設(Assumption)」,應明確詢問所有必要資訊 |
| G36 | 避免傳遞性導覽 (Transitive Navigation) | 也就是笛米特法則(Law of Demeter)。 避免寫出 a.getB().getC().doSomething() 這樣的程式碼,模組不應了解它所合作物件的內部細節 |
實作紀律#
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| G5 | 程式碼重複 (Duplication) | 這是萬惡之源。 重複代表著抽象化的缺失 |
| G9 | 死程式碼 (Dead Code) | 不會被執行的程式碼 (如永遠為 false 的 if 區塊)應被移除 |
| G10 | 垂直分隔 | 變數應定義在靠近使用處; 私有函式應定義在呼叫它的函式正下方 |
| G25 | 用具名常數取代魔術數字 | 讓數字擁有語意 |
意圖與表達#
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| G16 | 意圖模糊 | 程式碼應當簡潔且具表達力 |
| G19 | 變數應具解釋性 | 將複雜的計算結果存入具有良好命名的中繼變數中, 能讓邏輯更清晰 |
| G20 | 函式名稱應「說到做到」 | 名稱與行為必須一致,不能有誤導 |
| G28 | 封裝條件判斷 | 將複雜的 if 判斷式封裝成一個回傳布林值的函式(例如 if (shouldBeDeleted(timer))優於 if (timer.hasExpired() && !timer.isRecurrent())) |
| G29 | 避免否定條件判斷 | 正向思考較容易理解, 盡量避免 if (!isNotAvailable()) 這種雙重否定 |
Java 語言特性 (Java)#
針對 Java 語言的特定建議:
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| J1 | 善用萬用字元引入 | 若引入同一個套件的多個類別, 使用 import package.* 可以避免引入列表過長(註:這點在不同團隊可能有不同規範) |
| J2 | 不要繼承常數 | 應使用 static import 來引入常數,而非透過繼承介面來取得 |
| J3 | 善用 Enum | 盡量用列舉(Enum)取代 public static final int |
命名 (Names)#
好的命名能省下閱讀文件的時間。
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| N1 | 名稱具描述性 | 具描述性的名稱能大幅提升可讀性 |
| N2 | 依據抽象層次命名 | 不要讓實作細節洩漏到名稱中 (例如介面名為 IPhone 優於 PhoneInterface) |
| N3 | 使用標準命名法 | 若使用了設計模式(如 Factory, Observer), 請將其體現在名稱中,讓讀者能快速建立心智模型 |
| N4 | 名稱應無歧義 | 即便長度較長, 清晰度仍優於簡短但模糊的名稱 |
| N7 | 描述副作用 | 如果函式會改變狀態或做額外的事 (如 createOrReturn),名稱必須誠實反映 |
測試 (Tests)#
測試是系統品質的守門員。
| 代碼 | 壞味道 | 說明 |
|---|---|---|
| T1 | 測試不足 | 只要還有沒被驗證的邏輯或邊界條件,測試就不算足夠 |
| T2 | 使用覆蓋率工具 | 這是找出「未被測試代碼」最快的方法 |
| T3 | 別跳過簡單測試 | 即使邏輯簡單,撰寫測試的文件價值往往高於其成本 |
| T5 | 測試邊界條件 | 錯誤最喜歡躲在邊界(如 0, null, 空字串) |
| T6 | 錯誤往往群聚 | 當你在某個函式發現 Bug,通常附近還有更多 Bug |
| T9 | 測試要夠快 | 只有執行快速的測試,開發者才會願意頻繁地執行它 |
這份清單並非教條,而是啟發。
它幫我們寫程式時保持警覺,並持續追求更整潔、專業的程式碼品質。