🎯 什麼情境該想到我
分工:這張是分診表——聞到味道後先處理哪個、交給哪個工具自動抓。24 種壞味道逐一的偵測方法、Go 裡「其實不是味道」的情況、對應重構手法 →
reference/重構/(例:過長函式、重複程式碼、神秘命名、發散式變化、霰彈式修改、依戀情結)。
當你覺得某段程式碼「怪怪的、很髒」但講不出具體問題,或想判斷「該從哪裡開始重構」時。
⚙️ 怎麼用(把直覺變成可指認的清單)
常見壞味道 → 對應重構方向:
- 重複程式碼 → 提煉並共用
- 過長函式 / 過大類別 → 拆小(工具-提煉函式)
- 過長參數列 → 包成物件
- 發散式變化(一個類別因多種原因而改)/ 霰彈式修改(一個改動要動很多處)→ 重新歸位職責
- 依戀情結(方法愛用別的類別資料)→ 搬移函式
- 資料泥團 / 基本型別偏執 / 過度耦合的訊息鏈
★ 分診:先處理哪一種?
聞到很多味道時,依「擴散速度 × 修改頻率」排序:
| 優先 | 味道 | 為什麼先處理 |
|---|---|---|
| 🔴 高 | 重複程式碼、霰彈式修改 | 每次改需求都要改好幾處,漏改就是 bug。成本會隨時間線性增加 |
| 🔴 高 | 神秘命名 | 最便宜(工具一鍵改名)、效果最立即;而且改名本身常揭露其他問題 |
| 🟡 中 | 過長函式、發散式變化、依戀情結 | 讓改動變難,但不會自己擴散;在要改那一塊時順手處理 |
| 🟢 低 | 冗員元素、誇誇其談通用性 | 讀起來煩,但不會造成 bug |
| ⚪ 放著 | 在不會再改的程式碼裡的任何味道 | 重構的回報來自「之後的修改變便宜」;不改的程式碼沒有回報 |
最後一列最重要:分診的第一個問題永遠是「這段程式碼接下來會被改嗎?」用 git log --since=6.months --format= --name-only | sort | uniq -c | sort -rn | head 找出最常被改的檔案,味道 × 修改頻率高的才是真正的熱點。
★ 讓工具幫你聞:golangci-lint 的對應
以下 linter 名稱依 golangci-lint 官方清單(2026-09-25 查證):
| 味道 | linter | 抓什麼 |
|---|---|---|
| 過長函式 | funlen | 函式行數/敘述數超過門檻 |
| 過長函式(複雜度) | gocyclo、gocognit、nestif | 圈複雜度、認知複雜度、if 巢狀深度 |
| 重複程式碼 | dupl | 語法樹層級的重複片段 |
| 基本型別偏執(魔術值) | goconst、mnd | 重複出現的字串字面量、裸數字 |
| 冗員參數 | unparam | 永遠傳同一個值、或從不使用的參數 |
| 全域資料 | gochecknoglobals | 套件層級變數 |
用法建議:門檻不要一次設嚴。先用現況的第 90 百分位當門檻,只擋「新增的更糟的程式碼」,再逐步收緊(這個做法見 工具-破窗理論與軟體熵 的棘輪)。
TS 對應:ESLint 的 complexity、max-depth、max-lines-per-function、max-params 規則;重複偵測可用 jscpd。
★ 工具抓不到的味道
linter 只能看單一檔案的語法。跨檔案、跨時間的味道只能靠人:
- 霰彈式修改:看 PR 的檔案清單——一個小需求動了 12 個檔案,就是它。
- 發散式變化:
git log看同一個檔案被多少種不相關的需求改過。 - 依戀情結:方法裡
other.X、other.Y、other.Z比s.還多。
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
-
聞到味道不代表一定要立刻改;權衡成本與風險,並確保有測試。
-
把 linter 的數字當成目標,程式碼會變醜:為了把
funlen壓到 40 行而硬切出只被呼叫一次、名字說不清的函式,是把「過長函式」換成了「過度間接」。linter 是提醒你去看一眼,不是裁決。