📌 30 秒摘要(Layer 3)

重構=在不改變外部行為的前提下,改善程式內部結構。做法是「小步前進、每步都跑測試」,用一系列有名字的標準手法(提煉函式、改名、搬移…)逐步改善。判斷「哪裡該重構」靠嗅出程式碼壞味道。原則:要加功能時,先重構讓它容易加,再加。

🗺 心智圖(Canvas)

重構

🔧 重構

Refactoring — Martin Fowler 不改外部行為,只改內部結構

🎯 什麼情境該想到我

分工:這張是分診表——聞到味道後先處理哪個、交給哪個工具自動抓。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 是提醒你去看一眼,不是裁決。

🔗 相關工具

  • 工具-小步重構 —— 聞到味道之後的行動節奏,一次改一小塊
  • 工具-提煉函式 —— 對付最常見那幾種味道(過長函式、重複程式碼)的主力手法

🎯 什麼情境該想到我

分工:這張講「節奏與安全網」——多小算小、紅燈怎麼辦、大改動怎麼切。個別手法的步驟 → reference/重構/ 整份型錄(入口:重構);沒有測試的舊碼怎麼先補安全網 → 工具-特徵測試。

當你想改善程式結構、卻怕一改就壞掉時。

⚙️ 怎麼用

  1. 先確保有可靠測試(沒有就先補;見 工具-特徵測試)。
  2. 每次只做一個小改動(一個標準重構手法)。
  3. 每步都跑測試:綠燈才繼續,紅燈就退回上一步。
  4. 提交要頻繁:小步可回溯,出錯損失小。
  5. 重構與加功能分開做(別同時戴兩頂帽子)。

★ 「小步」有多小:三個可檢查的標準

  1. 每一步之後都能編譯、測試全綠。做到一半編譯不過的狀態,不算一步。
  2. 每一步都說得出是哪一個具名手法(改名、提煉函式、搬移函式、引入參數物件……)。說不出名字,通常是一次做了好幾件事。
  3. 每一步都值得一個 commit(至少在本機)。實務節奏大約幾分鐘一次;超過半小時還沒綠燈,步子就太大了。

★ 紅燈時的鐵律:退回,不要除錯

重構中測試變紅,直接 git checkout .(或 git stash)退回上一個綠燈,然後用更小的步子重做。

理由:重構的定義是「不改變行為」,紅燈代表你已經改到行為但不知道在哪。在一堆半改的程式碼裡除錯,通常比重做那 5 分鐘慢得多。這也是為什麼 commit 要頻繁——退回的成本 = 上次 commit 以來的工作量。

★ 測試不該跟著改

純重構時,測試程式碼理想上一行都不用動。如果每次重構都要改測試,代表測試綁在實作細節上(mock 了私有互動、斷言了內部呼叫順序),它就不是安全網,而是另一份需要同步維護的副本。例外:改的就是公開 API 的名字或簽名(例如 改變函式宣告),那測試當然要跟著改,但只能是機械性的改名。

★ 大改動怎麼切成小步:三個策略

策略什麼時候用做法
先讓改動變簡單,再做簡單的改動(Kent Beck 的說法)要加功能,但現有結構擋路先純重構到「新功能只要加幾行」的形狀,commit;再加功能,另一個 commit。兩頂帽子分開戴
平行改動(expand → migrate → contract)改 API 簽名、DB 欄位,呼叫端很多①新舊並存(加新函式/新欄位)→ ②呼叫端一個一個搬 → ③沒人用舊的了再刪。每個階段都可上線
抽象分支(branch by abstraction)換掉整個元件(ORM、HTTP client、儲存層)先在呼叫端與舊實作之間插一個介面 → 寫新實作 → 用設定切換 → 刪舊實作。全程在主幹上,不開長命分支

Go 的介面是隱式的,所以「插一個介面」幾乎零成本:在呼叫端的套件宣告一個只含用到方法的小介面,舊的 struct 自動就實作了它(見 工具-Go介面小而隱式)。

★ 工具:讓每一步由編輯器保證正確

  • Go 的 gopls 提供 rename、extract function/variable、inline 等重構(編輯器的 code action);rename 最可靠,因為它用型別資訊找所有引用。extract 類在邊界情況(defer、多個回傳值、閉包)比較不嚴謹,做完仍要跑測試。
  • 測試太慢不是跳過測試的理由,而是縮小範圍的理由:go test ./internal/billing/... -run 'TestInvoice' 只跑被影響的部分,commit 前再跑全部。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意

  • 沒測試就大改=走鋼索。先建立安全網再動手。

  • 別在同一個 commit 裡混「重構 + 行為改變」:code review 看不出哪一行是故意改行為的,出事時 git bisect 也無法區分。

  • 重構不是排進 sprint 的一張獨立工單:最有效的重構是「要改這裡的時候,順手先把這裡整理好」(見 工具-破窗理論與軟體熵 的童子軍規則)。

🔗 相關工具

🎯 什麼情境該想到我

分工:這張是快速決策卡——什麼時候提煉、名字怎麼取、何時收手。完整的機械步驟、Go 裡提煉時 defer/recover/內聯預算的陷阱、gopls 的支援程度 → 提煉函式(型錄頁);反向操作 → 行內函式。

當你有一段程式碼「可以獨立命名、說明它在做什麼」,或想拆長函式、消重複時。最常用的重構手法。

⚙️ 怎麼用

  1. 找出一段內聚的程式碼(常是「這段在算 X」)。
  2. 抽成新函式,用意圖命名(名字說「做什麼」而非「怎麼做」)。
  3. 把用到的區域變數當參數傳入,回傳需要的結果。
  4. 原處改成呼叫新函式,跑測試確認行為不變。

好處:消除重複、讓函式各層抽象一致、用函式名取代解釋性註解。

★ 什麼時候該提煉:四個訊號

訊號例子
你想在一段程式碼上面寫註解說它在做什麼// 計算運費 下面 8 行 → shippingFee(order)
同一段邏輯出現第三次兩次還可以忍,三次就是一個概念,該有名字(見 工具-DRY原則)
一個區塊的抽象層次跟周圍不同業務流程中間突然出現位元運算或字串切割
你看不懂一段程式碼,花了五分鐘終於懂了立刻把你的理解變成函式名,下一個人就不用再花五分鐘

★ 名字取「做什麼」,不取「怎麼做」

怎麼做(✗)做什麼(✓)
loopAndSumPrices(items)subtotal(items)
checkMapAndDB(id)findUser(id)
parseSplitTrim(s)parseTags(s)

判準:實作換掉之後,名字還正確嗎? findUser 不管查快取還是查 DB 都對;checkMapAndDB 一改實作就說謊。

★ 提煉後要檢查的三件事

  1. 參數個數:提煉出來的函式需要 5 個參數 → 切點選錯了,或這些資料其實該是一個 struct(引入參數物件)。
  2. 有沒有改到副作用順序:原本夾在兩個 I/O 中間的程式碼,搬出去後是否還在同一個時間點執行?尤其是 defer——在新函式裡的 defer 會在新函式結束時執行,而不是原函式結束時。
  3. 呼叫處讀起來像不像一句話:if isEligible(user, plan) ✓;if check(u, p, true, nil) ✗。

★ 何時收手(不要提煉)

  • 提煉出來的函式只有一行,而那一行本身已經很清楚(return a + b)。
  • 讀者每次都得跳進去才能理解呼叫處——名字沒有提供新資訊,只是多一次跳轉。
  • 為了提煉,得把 4 個區域變數改成指標參數傳進去修改:這代表這段邏輯和周圍狀態糾纏太深,先處理狀態(拆資料結構)再提煉。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意

  • 若抽出後參數爆多,代表切點不對或該連資料一起搬(考慮搬移到別的類別)。

🔗 相關工具

🧰 即用工具

🦨 24 個壞味道 [[神秘命名]]・[[重複程式碼]]・[[過長函式]]・[[過長參數列]]・[[全域資料]]・[[可變資料]]・[[發散式變化]]・[[霰彈式修改]]・[[依戀情結]]・[[資料泥團]]・[[基本型別偏執]]・[[重複的 switch]]・[[迴圈]]・[[冗員元素]]・[[誇誇其談通用性]]・[[暫時欄位]]・[[訊息鏈]]・[[中間人]]・[[內幕交易]]・[[過大類別]]・[[異曲同工的類別]]・[[純資料類別]]・[[被拒絕的遺贈]]・[[註釋]]

🦨 程式碼壞味道(24) 判斷「哪裡該重構」**

組織函式與變數(8) [[提煉函式]]・[[行內函式]]・[[提煉變數]]・[[行內變數]]・[[改變函式宣告]]・[[封裝變數]]・[[引入參數物件]]・[[拆分階段]]

封裝(7) [[封裝記錄]]・[[封裝集合]]・[[以物件取代基本型別]]・[[以查詢取代暫時變數]]・[[提煉類別]]・[[內聯類別]]・[[隱藏委託關係]]

搬移與迴圈(5) [[搬移函式]]・[[搬移欄位]]・[[拆分迴圈]]・[[以管道取代迴圈]]・[[移除死程式碼]]

簡化條件邏輯(5) [[分解條件式]]・[[合併條件式]]・[[以衛述句取代嵌套條件式]]・[[以多型取代條件式]]・[[引入特例]]

重構 API 與繼承(5) [[將查詢與修改分離]]・[[移除旗標參數]]・[[以子類取代型別碼]]・[[提煉超類]]・[[函式上移]]

🔧 主要重構手法(30) 「怎麼改」**

兩頂帽子 你要嘛在「加功能」、要嘛在「重構」,不要同時戴兩頂帽子。

先重構再加功能 如果加功能很難,先重構到「容易加」,再加。

前提是測試 沒有可靠測試就先補;遺留碼見 [[修改程式碼的藝術]]。

🧭 重構心法

Link to original

🧰 這本書給我的工具(即用快速工具)

📖 完整型錄(詳解在 reference/)

🦨 程式碼壞味道 Code Smells(24,判斷「哪裡該重構」)

🔧 主要重構手法 Refactorings(30,怎麼改)

組織函式與變數

封裝

搬移與迴圈

簡化條件邏輯

重構 API 與繼承

✨ 關鍵重點(Layer 1–2)

  • 定義:不改行為、只改結構;靠測試保證行為不變。
  • 兩頂帽子:你要嘛在「加功能」、要嘛在「重構」,別同時戴。
  • 先重構再加功能:「如果加功能很難,先重構到容易加,再加。」
  • 常見壞味道:重複程式碼、過長函式、過大類別、發散式變化、依戀情結、資料泥團…
  • 重構的前提是測試:沒有測試就先補(見 修改程式碼的藝術)。

💬 金句原文(Layer 0)

  • 「任何傻瓜都能寫出電腦看得懂的程式;優秀的程式設計師寫出人看得懂的程式。」
  • 「重構前,先確保有一組可靠的測試。」

🔗 相關