壞味道(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