重構的動機與策略
你有沒有修完一個 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 -5v0.28.0 重構前的結果顯示,run_git_command 在多個檔案中各有一份定義。這意味著:
- 修復一個 Bug 要改 4 個地方
- 漏改任何一個就會產生行為不一致
- 新增 Hook 時需要再複製一份
重複程式碼的總行數約 415 行。這 415 行不只是浪費空間——它們是 415 行的維護風險。
判斷三問
在決定是否重構之前,問自己三個問題:
- 讀這段程式碼時,我需要同時記住多少東西? 超過 7 個就是警訊。
- 如果要修改一個行為,我需要改幾個地方? 超過 1 個就有重複的問題。
- 新人加入團隊後,需要多久才能理解這段程式碼? 如果答案是「需要有人口頭解釋」,那就是程式碼本身不夠清楚。
如果三個問題的答案都指向問題,那就該動手了。
何時不應該重構
不是所有情況都適合重構。以下三個時機,重構通常會帶來更多問題。
沒有測試保護時
重構的前提是:你能驗證修改後的行為和修改前一致。沒有測試,你就無法確認這一點。
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 和重構是兩件不同的事。混在一起做會產生兩個問題:
- 難以驗證:Bug 修好了嗎?還是被重構掩蓋了?
- 難以回溯:如果重構引入了新問題,
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.0 | v0.31.0 |
|---|---|---|
| 性質 | 建立新架構 | 統一既有架構的風格 |
| 風險來源 | 介面設計是否正確 | 機械性修改是否有隱含的邏輯變更 |
| Wave 數 | 4(全部計畫內) | 4(第 4 個是意外新增) |
| 教訓 | 先建基礎設施再動手 | 機械性修改也可能觸發邏輯問題 |
Wave 分解的原則
從兩次重構經驗中,可以歸納出三個分解原則:
原則 1:依賴方向決定順序
被依賴的模組先做。v0.28.0 先建共用程式庫(Wave 1),因為後續所有 Wave 都會用到它。
1Wave 1: 共用模組(被所有 Hook 依賴)
2 ↓
3Wave 2: 配置檔(被 task-dispatch 依賴)
4 ↓
5Wave 3: Hook 檔案(依賴共用模組和配置檔)
6 ↓
7Wave 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 後 |
|---|---|---|---|
| 共用模組數 | 0 | 4 | 7+ |
| 重複程式碼行數 | ~415 行 | ~0 | ~0 |
| 新增 Hook 的樣板行數 | ~15 行 | ~10 行 | ~5 行(核心呼叫 1 行) |
| Error Patterns 記錄數 | 4 | 8 | 19 |
| task-dispatch 行數 | 858 | 296 | 267 |
幾個值得注意的趨勢:
- 共用模組數從 0 成長到 7+。這代表重複程式碼有了歸屬,不再散落各處。
- 新增 Hook 的樣板行數從 15 行降到約 5 行(核心只需 1 行匯入呼叫)。新增一個 Hook 從「複製一堆工具函式」變成「匯入共用模組」。
- Error Patterns 從 4 成長到 19。這不是壞事——它代表團隊開始系統性地記錄和傳承經驗,而不是每個人各自出問題。
小結
重構的決策可以歸納為三個問題:
- 該不該做? 認知負擔超載、重複程式碼累積、維護成本持續上升,就該做。
- 現在能做嗎? 有測試保護、不在修 Bug、時間充裕,才能做。
- 怎麼拆分? 按依賴順序,機械和邏輯分開,每步結束都可用。
後續章節會深入每個具體的重構技巧:如何識別壞味道、如何抽取配置、如何消除重複、如何處理作用域陷阱。
思考題
你目前的專案中,有哪些檔案的行數超過 200 行?列出前三名,分析它們為什麼會這麼長——是職責太多、重複程式碼、還是配置和邏輯混在一起?
回想一次你在修 Bug 時順手重構的經驗。事後回頭看
git log,能清楚區分哪些變更是修 Bug、哪些是重構嗎?如果你的專案完全沒有測試,但認知負擔已經很高,你會怎麼規劃「補測試」和「重構」的先後順序?
實作練習
用以下指令掃描你的專案,找出重複定義的函式:
1# 在專案根目錄執行 2grep -h "^def " your_project/*.py | sort | uniq -c | sort -rn | head -10分析結果:哪些函式被重複定義了?它們應該被抽到哪個共用模組?
選一個超過 200 行的檔案,嘗試畫出它的 Wave 分解計畫。回答以下問題:
- 哪些部分被其他部分依賴?(先做)
- 哪些修改是機械性的?(可以批量處理)
- 每個 Wave 完成後,程式碼能正常執行嗎?
計算你選定檔案的認知負擔指數(變數數 + 分支數 + 巢狀深度 + 依賴數)。找出指數最高的函式,思考如何將它拆分到指數低於 10。
下一章:程式碼壞味道偵測
文件版本:v0.31.0 最後更新:2026-03-04