壞味道(Code Smell)是「程式碼可能有問題」的表面信號。它不是 bug,程式照樣能跑;但它提示某處的設計正在腐化,往往是該動手 重構 的入口。
壞味道只是「線索」而非「罪證」。聞到味道要問的是「這裡為什麼難改?」,再決定要不要重構,而不是看到就無腦修。
壞味道與重構的關係#
壞味道告訴你「哪裡可能不對」,重構手法告訴你「怎麼修」。兩者是一體兩面:
flowchart LR
A[聞到壞味道] --> B{影響可維護性?}
B -->|是| C[選擇對應重構手法]
B -->|否| D[標記/暫不處理]
C --> E[小步重構]
E --> F[測試保護下驗證行為不變]
F --> A還沒有測試罩住的程式碼,先補 characterization test 再動手。詳見 接縫與遺留碼安全重構。
壞味道分組總覽#
依照「問題的本質」可把常見壞味道分為五大群:
mindmap
root((Code Smells))
命名與膨脹
神秘命名
過長函式
過大類別
過多註解
重複與擴散
重複程式碼
發散式修改
散彈式修改
重複的 switch
資料設計
資料群 Data Clumps
基本型別偏執
資料類別
暫時欄位
耦合與物件協作
特性嫉妒
訊息鏈
中間人
內線交易
繼承與通用化
被拒絕的遺贈
推測性通用化
平行繼承體系命名與膨脹類#
| 壞味道 | 症狀 | 對應重構手法 |
|---|---|---|
| 神秘命名(Mysterious Name) | 名稱無法表達意圖,需讀實作或註解才懂 | Rename Variable / Method / Field |
| 過長函式(Long Function) | 函式超過一個螢幕、混雜多個抽象層級 | Extract Method、以查詢取代暫存變數、分解條件 |
| 過大類別(Large Class) | 類別超過 500 行、欄位過多、職責不清 | Extract Class、Extract Subclass |
| 過多註解(Comments) | 用註解解釋糟糕的程式碼,而非改善它 | Extract Method、Rename、Introduce Assertion |
註解過多常是「程式碼在求救」。先嘗試用命名與抽取讓程式碼自我解釋,剩下真正需要說明「為什麼」的,才留成註解。
// 壞味道:神秘命名 + 過多註解
// 計算含稅總額(稅率 5%)
double calc(double p, int q) {
return p * q * 1.05;
}
// 改善:命名常數 + 自解釋
private static final double TAX_RATE = 0.05;
double totalWithTax(double unitPrice, int quantity) {
double subtotal = unitPrice * quantity;
return subtotal * (1 + TAX_RATE);
}重複與擴散類#
| 壞味道 | 症狀 | 對應重構手法 |
|---|---|---|
| 重複程式碼(Duplicated Code) | 相同或近似邏輯出現多處 | Extract Method、Pull Up Method、Form Template Method |
| 發散式修改(Divergent Change) | 一個類別因多種不同原因而頻繁被改 | Extract Class(依變更原因拆分) |
| 散彈式修改(Shotgun Surgery) | 一個變更要同時改很多個類別 | Move Method / Field、Inline Class(把分散邏輯收攏) |
| 重複的 switch(Repeated Switches) | 同一組 switch/if-else 散落多處 | Replace Conditional with Polymorphism |
發散式修改與散彈式修改是「對偶」:前者是一個類別被太多原因改動(內聚不足),後者是一次改動波及太多類別(過度分散)。方向相反,但都是模組邊界沒切對。
// 重複的 switch:型別判斷散落各處
double pay(Employee e) {
switch (e.type) {
case ENGINEER: return e.base;
case MANAGER: return e.base + e.bonus;
default: throw new IllegalArgumentException();
}
}
// 改善:以多型取代條件
abstract class Employee {
abstract double pay();
}
class Engineer extends Employee {
double pay() { return base; }
}
class Manager extends Employee {
double pay() { return base + bonus; }
}資料設計類#
| 壞味道 | 症狀 | 對應重構手法 |
|---|---|---|
| 資料群(Data Clumps) | 同一組欄位/參數總是一起出現 | Extract Class、Introduce Parameter Object |
| 基本型別偏執(Primitive Obsession) | 用 String/int 表達電話、金額、座標等概念 | Replace Primitive with Object、Replace Type Code with Class |
| 資料類別(Data Class) | 只有欄位與 getter/setter、沒有行為 | Move Method(把行為搬進來)、封裝集合 |
| 暫時欄位(Temporary Field) | 欄位只在特定情況下才有值 | Extract Class、Introduce Special Case(Null Object) |
// 資料群 + 基本型別偏執
void shipTo(String street, String city, String zip) { ... }
// 改善:參數物件 + 值物件
class Address {
private final String street;
private final String city;
private final ZipCode zip; // ZipCode 自帶驗證
// 建構時驗證不變量
}
void shipTo(Address address) { ... }資料群是「值物件」的提示:當三個欄位老是綁在一起傳,它們大概是一個尚未誕生的概念。抽成物件後,相關驗證與行為也有了歸宿。
耦合與物件協作類#
| 壞味道 | 症狀 | 對應重構手法 |
|---|---|---|
| 特性嫉妒(Feature Envy) | 方法對「別的類別」的資料比自己的還感興趣 | Move Method、Extract Method 後搬移 |
| 訊息鏈(Message Chains) | a.getB().getC().getD() 連環取值 | Hide Delegate、Extract Method |
| 中間人(Middle Man) | 類別大半方法只是轉呼叫給別人 | Remove Middle Man、Inline Method |
| 內線交易(Insider Trading) | 模組間私下交換太多內部細節 | Move Method / Field、以中間人正當化交易 |
Hide Delegate(隱藏委託)與 Remove Middle Man(移除中間人)方向相反。隱藏委託多了會變成中間人,移除中間人過頭又出現訊息鏈——這是需要持續微調的平衡,不是一次到位。
// 特性嫉妒:方法一直在算 customer.address 的東西
class Order {
String shippingLabel(Customer customer) {
return customer.getAddress().getCity()
+ " " + customer.getAddress().getZip();
}
}
// 改善:把行為搬到資料所在的類別
class Customer {
String shippingLabel() {
return address.getCity() + " " + address.getZip();
}
}繼承與通用化類#
| 壞味道 | 症狀 | 對應重構手法 |
|---|---|---|
| 被拒絕的遺贈(Refused Bequest) | 子類別不想要父類別的方法/欄位 | Push Down Method/Field、以委託取代繼承 |
| 推測性通用化(Speculative Generality) | 為「將來可能用到」預埋抽象與掛鉤 | Collapse Hierarchy、Inline Class、移除未用參數 |
| 平行繼承體系(Parallel Inheritance) | 新增一個子類別就被迫在另一體系也加一個 | Move Method / Field 合併兩個體系 |
推測性通用化是 YAGNI 的反例。看到只有單一實作的抽象介面、永遠只走 default 的 hook、從沒被第二個呼叫者用過的參數,多半可以拿掉。詳見 設計原則。
如何使用這份目錄#
- 不要追求「零味道」:味道是權衡訊號,有些在當前脈絡下是合理的取捨。
- 以「修改成本」為準:當某個味道讓你「不敢改、改不動、改了會壞」,它才值得處理。
- 配對使用:聞到味道 ➡️ 查對應手法 ➡️ 在測試保護下小步重構。
壞味道快速索引(症狀 ➡️ 手法)
| 你觀察到的現象 | 可能的壞味道 | 先試的手法 |
|---|---|---|
| 看不懂名字 | 神秘命名 | Rename |
| 函式滾不完螢幕 | 過長函式 | Extract Method |
| 同段邏輯貼了三次 | 重複程式碼 | Extract Method / Pull Up |
| 改一處要連改十處 | 散彈式修改 | Move Method / Field |
| 一個類別什麼都改 | 發散式修改 | Extract Class |
一串 .getX().getY() | 訊息鏈 | Hide Delegate |
| 方法老在用別人的資料 | 特性嫉妒 | Move Method |
| 一堆參數總一起傳 | 資料群 | Introduce Parameter Object |
| 用 String 表示金額/電話 | 基本型別偏執 | Replace Primitive with Object |
| 介面只有一個實作 | 推測性通用化 | Inline / Collapse Hierarchy |