你有沒有修完一個 Bug,一週後發現另一個地方有完全一樣的問題?或者改了一個函式的行為,卻不確定還有哪些地方依賴它?這些情境的根源往往是程式碼結構讓問題難以被看見。

重構是改變程式碼的內部結構,而不改變其外部行為。聽起來簡單,但實務上最困難的問題不是「怎麼改」,而是「為什麼改」和「什麼順序改」。

本章從 Hook 系統兩次大規模重構(v0.28.0 和 v0.31.0)的經驗出發,討論重構的動機判斷和階段分解策略。Hook 系統是一個由數十個 Python 腳本組成的自動化系統,負責程式碼品質檢查、流程驗證和開發規範執行。

為什麼要重構

認知負擔超載

重構的第一個訊號是:讀程式碼時,你需要同時記住太多東西。

v0.28.0 重構前,task-dispatch-readiness-check.py 有 858 行。閱讀這個檔案時,你需要同時追蹤:

  • 15 個代理人的名稱和觸發條件
  • Git 分支操作的 subprocess 細節
  • Worktree 路徑解析邏輯
  • 重複出現的工具函式(同一個 run_git_command 在多個檔案中各自定義一次)

心理學家 George Miller 的研究指出,人類的工作記憶一次只能處理 7 加減 2 個項目。858 行的檔案遠超這個限制。你不可能在腦中同時維護這麼多上下文來理解程式碼的行為。

我們可以用一個簡單的公式量化這個問題:

1認知負擔指數 = 變數數 + 分支數 + 巢狀深度 + 依賴數
指數評估行動
1-5優良維持
6-10可接受考慮最佳化
11-15需重構排入計畫
> 15必須重構立即處理

v0.28.0 重構前的 task-dispatch-readiness-check.py,光是頂層函式就有 23 個,模組級變數超過 10 個。認知負擔指數遠超 15,屬於「必須重構」等級。

不重構的代價

「能動就不要碰」是常見的想法,但不重構的代價會隨時間累積。

v0.31.0 的 W24 開發週期提供了一個具體案例。任務是統一 16 個 Hook 檔案的 logger 初始化風格——看起來是一個簡單的機械性修改。但因為缺乏共用模組和清晰的模組邊界,修改引發了一個作用域問題(將全域變數移入函式後,其他引用該變數的函式失去存取權限),導致 7 個 Hook 靜默失敗,影響 41 個函式。更糟的是,這個問題至少持續了 2 個 session 才被發現。

完整的作用域迴歸分析參見作用域迴歸案例研究

靜默失敗比直接報錯更危險。錯誤被頂層的 try/except 吞掉,只寫入日誌檔案,而沒有人在看日誌。如果重構前就有清晰的模組邊界和完整的測試覆蓋,這個問題可以在修改當下就被偵測到。

重複程式碼的連鎖效應

另一個推動重構的因素是重複。我們用一行指令就能量化問題的嚴重程度:

1# 在專案根目錄執行
2grep -h "^def " .claude/hooks/*.py | sort | uniq -c | sort -rn | head -5

v0.28.0 重構前的結果顯示,run_git_command 在多個檔案中各有一份定義。這意味著:

  • 修復一個 Bug 要改 4 個地方
  • 漏改任何一個就會產生行為不一致
  • 新增 Hook 時需要再複製一份

重複程式碼的總行數約 415 行。這 415 行不只是浪費空間——它們是 415 行的維護風險。

判斷三問

在決定是否重構之前,問自己三個問題:

  1. 讀這段程式碼時,我需要同時記住多少東西? 超過 7 個就是警訊。
  2. 如果要修改一個行為,我需要改幾個地方? 超過 1 個就有重複的問題。
  3. 新人加入團隊後,需要多久才能理解這段程式碼? 如果答案是「需要有人口頭解釋」,那就是程式碼本身不夠清楚。

如果三個問題的答案都指向問題,那就該動手了。

何時不應該重構

不是所有情況都適合重構。以下三個時機,重構通常會帶來更多問題。

沒有測試保護時

重構的前提是:你能驗證修改後的行為和修改前一致。沒有測試,你就無法確認這一點。

1# 假設你想把這段程式碼抽成函式
2if len(result.stdout.strip()) > 0:
3    branches = result.stdout.strip().split("\n")
4    branches = [b.strip() for b in branches if b.strip()]
5    protected = [b for b in branches if b in ["main", "master"]]

這段邏輯涉及空字串處理、換行符分割、空白清理。如果沒有測試覆蓋這些邊界情況,抽取函式時很容易改變行為而不自知。

原則:先寫測試,再重構。如果時間只夠做一件事,選擇寫測試。

修 Bug 時順手重構

修 Bug 和重構是兩件不同的事。混在一起做會產生兩個問題:

  1. 難以驗證:Bug 修好了嗎?還是被重構掩蓋了?
  2. 難以回溯:如果重構引入了新問題,git bisect 無法區分哪些變更是修 Bug、哪些是重構
1# 錯誤的工作流程
2commit: "修復 #42 並重構 git_utils"  ← 兩件事混在一個 commit
3
4# 正確的工作流程
5commit: "修復 #42:branch 名稱解析的空字串處理"
6commit: "重構 git_utils:抽取 parse_branch_name 函式"

原則:先修好 Bug 並提交,確認測試通過,然後再開始重構。

時間壓力下

v0.31.0 W24 的 logger 統一修改就是一個教訓——在時間壓力下跳過了跨函式引用的完整驗證,結果 7 個 Hook 靜默失敗,修復花費的時間遠超原本省下的。

重構需要完整的注意力。趕進度時進行重構,容易在壓力下跳過驗證步驟(「測試之後再補」),反而製造更多技術債務。

原則:記錄下需要重構的地方,排入後續計畫。不要在時間壓力下動手。

判斷清單

把上述三個情境整理成一個快速檢查清單:

問題
有測試覆蓋修改區域?可以繼續先寫測試
修改範圍只有重構?可以繼續先分開 Bug 修復和重構
有足夠的時間完成驗證?可以繼續記錄後排入計畫

三個問題都回答「是」,才開始動手。

Wave 分解策略

大規模重構最容易失敗的原因是:試圖一次做完所有事情。

Wave:一個有明確目標和驗證點的重構階段。每完成一個 Wave,程式碼都必須處於可用狀態。

Wave 分解的核心思想是:把重構拆成多個有序的 Wave,確保每一步都可驗證、可回退。

v0.28.0:基礎架構重構

v0.28.0 將 Hook 系統從「各自為政」重構為「共用模組 + 配置分離」的架構。拆分為 4 個 Wave:

Wave 1:建立共用程式庫

先建立共用模組的介面和測試,不改動任何現有 Hook。

1建立模組    →  寫測試    →  確認測試通過
2config_loader    4 個測試
3git_utils        6 個測試
4hook_io          3 個測試
5hook_logging     2 個測試

為什麼先做這步?因為後續 Wave 需要依賴這些模組。如果跳過這步直接改 Hook,會遇到「要用的函式還不存在」的問題。

Wave 2:配置分離

把 task-dispatch 中的硬編碼清單(代理人名稱、品質規則、指令對應表)抽到 YAML 配置檔。這是行數最多、修改範圍最大的階段。

1# 重構前:硬編碼在 Python 中
2AGENTS = {
3    "parsley-flutter-developer": {"type": "implementation", "lang": "dart"},
4    "thyme-python-developer": {"type": "implementation", "lang": "python"},
5    # ... 15 個代理人定義散落在程式碼中
6}
7
8# 重構後:讀取 YAML 配置
9agents = config_loader.load_config("agents.yaml")

分離後,新增代理人只需要編輯 YAML 檔案,不需要動 Python 程式碼。

Wave 3:逐檔重構

有了共用程式庫和配置檔,逐一修改 Hook 檔案。每改完一個檔案就執行測試,確保沒改壞東西。這個階段的關鍵是紀律:每次只改一個檔案,改完就跑測試,不要累積多個修改後一起驗證。

Wave 4:驗證與清理

28 個單元測試全部通過。移除不再需要的重複程式碼。檢查是否有遺漏的相依性。

v0.31.0:風格統一與防護強化

v0.31.0 的重構是在既有架構上統一風格和強化防護,規模與性質都與 v0.28.0 不同。拆分為 4 個 Wave(W22-W25):

Wave目標性質
W22日誌系統統一機械性修改
W23訊息常數抽取機械性修改
W24程式碼風格統一機械性,但觸發了作用域問題
W25修復 W24 問題 + 防護機制修復 + 新功能

注意 W25 的存在。它不在原始計畫中,而是 W24 出問題後臨時新增的。好的分解策略要預留處理意外的空間。

兩次重構的對比:

維度v0.28.0v0.31.0
性質建立新架構統一既有架構的風格
風險來源介面設計是否正確機械性修改是否有隱含的邏輯變更
Wave 數4(全部計畫內)4(第 4 個是意外新增)
教訓先建基礎設施再動手機械性修改也可能觸發邏輯問題

Wave 分解的原則

從兩次重構經驗中,可以歸納出三個分解原則:

原則 1:依賴方向決定順序

被依賴的模組先做。v0.28.0 先建共用程式庫(Wave 1),因為後續所有 Wave 都會用到它。

1Wave 1: 共用模組(被所有 Hook 依賴)
23Wave 2: 配置檔(被 task-dispatch 依賴)
45Wave 3: Hook 檔案(依賴共用模組和配置檔)
67Wave 4: 驗證(依賴所有修改完成)

原則 2:機械性修改和邏輯修改分開

機械性修改(如統一命名風格、統一匯入路徑)和邏輯修改(如改變函式行為、改變變數作用域)不該放在同一個 Wave。

v0.31.0 的 W24 表面上是機械性修改(統一 logger 初始化位置),但實際上涉及了作用域的邏輯變更——把全域變數移入函式,會影響所有引用它的其他函式。如果在規劃時就識別出這一點,應該把「移動 logger 位置」和「修改函式簽名以傳遞 logger 參數」拆成兩個步驟。

怎麼區分?問自己:這個修改會不會改變任何函式的可見變數? 如果會,就不是純機械性修改。

原則 3:每個 Wave 結束時程式碼必須可用

不能出現「改到一半,程式跑不起來」的狀態。每個 Wave 完成後:

  • 所有測試通過
  • 程式碼可正常執行
  • 可以安全地提交

這保證了即使中途需要停下來處理其他事情,程式碼也不會處於損壞狀態。

分解流程

把上述原則整理成一個可操作的流程:

 1Step 1: 畫出依賴關係圖
 2    → 哪些模組被其他模組依賴?(先做)
 3    → 哪些模組互相獨立?(可以並行)
 4
 5Step 2: 分類修改類型
 6    → 機械性修改:統一命名、統一格式、統一匯入
 7    → 邏輯修改:改變函式行為、改變資料流、改變作用域
 8
 9Step 3: 分配到 Wave
10    → Wave N: 基礎設施(被依賴的、獨立的)
11    → Wave N+1: 機械性修改(依賴基礎設施)
12    → Wave N+2: 邏輯修改(依賴前面的修改)
13    → Wave N+3: 驗證與清理
14
15Step 4: 每個 Wave 定義驗證標準
16    → 哪些測試必須通過?
17    → 程式碼能正常執行嗎?
18    → 可以安全提交嗎?

度量表

用量化指標驗證重構是否達到目標:

指標v0.28.0 前v0.28.0 後v0.31.0 後
共用模組數047+
重複程式碼行數~415 行~0~0
新增 Hook 的樣板行數~15 行~10 行~5 行(核心呼叫 1 行)
Error Patterns 記錄數4819
task-dispatch 行數858296267

幾個值得注意的趨勢:

  • 共用模組數從 0 成長到 7+。這代表重複程式碼有了歸屬,不再散落各處。
  • 新增 Hook 的樣板行數從 15 行降到約 5 行(核心只需 1 行匯入呼叫)。新增一個 Hook 從「複製一堆工具函式」變成「匯入共用模組」。
  • Error Patterns 從 4 成長到 19。這不是壞事——它代表團隊開始系統性地記錄和傳承經驗,而不是每個人各自出問題。

小結

重構的決策可以歸納為三個問題:

  1. 該不該做? 認知負擔超載、重複程式碼累積、維護成本持續上升,就該做。
  2. 現在能做嗎? 有測試保護、不在修 Bug、時間充裕,才能做。
  3. 怎麼拆分? 按依賴順序,機械和邏輯分開,每步結束都可用。

後續章節會深入每個具體的重構技巧:如何識別壞味道、如何抽取配置、如何消除重複、如何處理作用域陷阱。

思考題

  1. 你目前的專案中,有哪些檔案的行數超過 200 行?列出前三名,分析它們為什麼會這麼長——是職責太多、重複程式碼、還是配置和邏輯混在一起?

  2. 回想一次你在修 Bug 時順手重構的經驗。事後回頭看 git log,能清楚區分哪些變更是修 Bug、哪些是重構嗎?

  3. 如果你的專案完全沒有測試,但認知負擔已經很高,你會怎麼規劃「補測試」和「重構」的先後順序?

實作練習

  1. 用以下指令掃描你的專案,找出重複定義的函式:

    1# 在專案根目錄執行
    2grep -h "^def " your_project/*.py | sort | uniq -c | sort -rn | head -10

    分析結果:哪些函式被重複定義了?它們應該被抽到哪個共用模組?

  2. 選一個超過 200 行的檔案,嘗試畫出它的 Wave 分解計畫。回答以下問題:

    • 哪些部分被其他部分依賴?(先做)
    • 哪些修改是機械性的?(可以批量處理)
    • 每個 Wave 完成後,程式碼能正常執行嗎?
  3. 計算你選定檔案的認知負擔指數(變數數 + 分支數 + 巢狀深度 + 依賴數)。找出指數最高的函式,思考如何將它拆分到指數低於 10。


下一章:程式碼壞味道偵測

文件版本:v0.31.0 最後更新:2026-03-04