程式碼的壞味道 (Bad Smells in Code) #
Martin Fowler 提出的「程式碼壞味道(Code Smells)」並非指程式碼有 Bug,而是指程式碼結構中可能存在深層設計問題的跡象。 識別這些味道,是進行重構的第一步。本章列出常見的壞味道及其對應重構策略。
1. 命名與基本邏輯 (Naming & Basics) #
程式碼的可讀性始於命名與去重。
| Code Smell | 症狀與根本問題 | Refactoring | 核心筆記/重要性 |
|---|---|---|---|
| Mysterious Name | 苦思不出好名稱;代表對功能理解不足或設計有缺陷 | Rename | 好名稱是程式碼清晰的關鍵,能大幅降低理解成本 |
| Duplicated Code | 在不同地點看到相同的程式結構或邏輯 | 提取函式 (Extract Function),將相同邏輯統一起來 | 違反 DRY 原則 (Don’t Repeat Yourself),是維護的夢魘 |
| Comments | 註釋被用來解釋複雜、難懂的程式碼,而非解釋「為什麼這麼做」 | 將邏輯抽為函式,或用斷言 (Assertion) 說明狀態規則 | 優秀的程式碼應該是自解釋的,註釋不應彌補不良設計 |
註釋往往像是「除臭劑」,用來掩飾程式碼的低劣品質。
如果你覺得需要寫註釋來解釋程式碼在做什麼,不如先試著重構它。
2. 函式與參數 (Functions & Parameters) #
函式應專注於「做什麼(What)」而非展示冗長的「如何做(How)」。
| Code Smell | 症狀 | Refactoring | 核心筆記與優化效益 |
|---|---|---|---|
| Long Function (過長函式) | 函式包含過多行數 | 拆分成小函式並給予良好名稱 | 長度不是絕對標準,重點在於「意圖 (What)」與「實作 (How)」的語意距離。小函式具備解釋性、共用性與選擇性 |
| Long Parameter List (過長參數列) | 函式需要傳入過多參數 | 將多個參數組合成一個物件或參數類別 (Parameter Object) | 過長的參數列會增加函式的複雜度和呼叫成本,組合成物件可提高可讀性和維護性 |
3. 資料與變數管理 (Data Handling) #
資料的範圍與型態管理不當,是導致系統脆弱的主因。
| Code Smell | 症狀 | Refactoring | 核心筆記與優化效益 |
|---|---|---|---|
| Global Data (全域資料) | 資料可在任何地方被修改,卻無法輕易追蹤是誰改的 | 用存取函式 (Accessor) 封裝它。封裝能限縮範圍,進而監控修改來源 | |
| Mutable Data (可變資料) | 變數的值頻繁變動,作用域越大,風險越高 | 封裝變數;將查詢與修改分離。呼應 Functional Programming 理念,盡可能保持資料不可變 (Immutable) | |
| Data Clumps (資料泥團) | 某些資料項目 (如:地址、城市、郵遞區號) 總是成群結隊地一起出現 | 將它們提取並集合成一個獨立的類別 | |
| Primitive Obsession (基本型態依戀) | 過度使用基本型態 (int, string) 來代表具備業務意義的概念 (如:電話號碼、貨幣) | 將其改為「數值物件 (Value Object)」 | 效益: 賦予資料明確意義,並可重用相關的驗證或處理函式 |
4. 變更的擴散與隔離 (Change Prevention) #
當需求變更時,好的設計應能將修改範圍限縮在單一模組內。
| Code Smell | 症狀 | Refactoring | 核心概念與目標 |
|---|---|---|---|
| Divergent Change (發散式修改) | 一個模組 (類別) 經常因為**「不同的原因」而被以「不同的方式」修改** | 拆分邏輯成不同階段 (Phase) 或類別,建立明確的領域邊界 | 違反單一職責原則 (SRP),一個模組應該只有一個修改理由 |
| Shotgun Surgery (散彈式修改) | 為完成一個小修改,須同時更動許多不同的類別 | 使用 Inline 重構或是搬移函式,將相關邏輯集中 | 相關邏輯被不當地分散,應將其聚集起來,以利維護 |
比較: * 發散式修改是「一個類別受多種變化影響」。
- 散彈式修改是「一種變化影響多個類別」。
5. 類別與物件導向濫用 (Object-Orientation Abusers) #
這些味道顯示程式碼未能有效利用物件導向的特性。
| Code Smell | 症狀與根本問題 | Refactoring | 核心概念與優化方向 |
|---|---|---|---|
| Repeated Switches (重複的 Switch) | 相同的 Switch 邏輯散落在各處,新增條件時,所有 Switch 都須修改 | 使用多型 (Polymorphism) 取代條件判斷 | 遵循開放/封閉原則 (OCP),對擴展開放,對修改封閉 |
| Large Class (過大類別) | 類別擁有太多欄位或職責。 | 依據使用方法,將其拆解為數個子類別或元件 | 違反單一職責原則 (SRP),提高內聚力 (Cohesion) |
| Alternative Classes with Different Interfaces (異曲同工的類別) | 兩個類別做類似的事,但函式命名或介面不同 | 更改函式簽名、移動函式,或提取共同父類別 | 提高一致性,使類別可互換,方便使用者 |
| Refused Bequest (被拒絕的遺贈) | 子類別繼承了父類別,卻只需要一小部分功能,或覆寫掉不需要的方法 | 建立旁系 (Sibling) 類別,或將繼承關係轉為委託 (Delegation) | 違反 Liskov 替換原則,繼承關係不恰當 |
| Temporary Field (暫時欄位) | 類別中的某些欄位,只在「特定情況」下才有值,其他時候是空的 | 將這些欄位與相關行為放入一個新類別 | 類別的欄位應該在所有情況下都有效,確保物件的狀態始終是完整的 |
6. 模組間的耦合 (Coupling) #
模組間應低耦合,避免過度親密或不必要的傳話。
| Code Smell | 症狀與根本問題 | Refactoring | 核心概念與目標 |
|---|---|---|---|
| Feature Envy (依戀情結) | 一個函式對**「別的類別」的資料**感興趣的程度,超過對自己所在類別的興趣。 | 將該函式(或部分邏輯)移到它該去的地方(資料所在的類別)。 | 提高內聚力,確保行為靠近數據。 |
| Message Chains (訊息鏈) | 程式碼像長鏈一樣層層呼叫(如 a.getB().getC().doSomething())。 | 隱藏委託關係 (Hide Delegate)。最好直接把該動作抽取成函式,而非僅是縮短鏈條。 | 違反迪米特法則 (Law of Demeter),增加耦合性。 |
| Middle Man (中間人) | 某個類別有一半以上的函式都只是在委託給另一個類別做事。 | 移除中間人,直接讓呼叫方與實作方溝通;或使用繼承。 | 降低不必要的間接層,減少重複委託。 |
| Insider Trading (內線交易) | 模組之間過度且隱晦地交換資料,破壞封裝。 | 搬移函式或使用中介模組來斷開緊密耦合。 | 確保模組之間的溝通清晰、公開,不破壞資訊隱藏原則。 |
7. 其他可疑之處 (Others) #
| Code Smell | 症狀與根本問題 | Refactoring | 核心概念與目標 |
|---|---|---|---|
| Loops (迴圈) | 使用傳統迴圈處理集合資料 (Collection) | 使用 Pipeline 操作(如 filter, map 等高階函式) | 宣告式的 Pipeline 通常比指令式的迴圈更易讀,符合函式式編程風格 |
| Lazy Element (冗贅元素) | 為了「將來可能有用」而存在的函式或類別,目前沒什麼作用或只有一行程式碼 | 使用 Inline 函式或類別將其消除 | 應保持程式碼精簡,避免不必要的抽象層級 |
| Speculative Generality (誇誇其談未來性) | 為了處理「未來可能出現的情況」而設計的複雜機制,目前只有測試程式在用 | 移除它們 | 遵循 YAGNI 原則 (You Ain’t Gonna Need It),只實現現在需要的東西 |
| Data Class (純資料類別) | 類別只擁有欄位與 Getter/Setter,沒有行為 | 檢查誰在使用這些資料,將相關的行為搬移進這個類別中 | 違反物件導向的封裝原則,數據應與其操作行為結合 |
如何看待壞味道
識別壞味道不是為了追求完美,而是為了風險管理。
當你聞到味道時,不代表必須立即修改,但它是個強烈訊號,告訴你這裡未來可能會出問題,或這裡是難以維護的熱點。