🎯 什麼情境該想到我

分工:這張講「節奏與安全網」——多小算小、紅燈怎麼辦、大改動怎麼切。個別手法的步驟 → 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 的一張獨立工單:最有效的重構是「要改這裡的時候,順手先把這裡整理好」(見 工具-破窗理論與軟體熵 的童子軍規則)。

🔗 相關工具