本章是模組七的總結。前面九章從動機判斷開始,依序教了壞味道識別、配置分離、DRY 原則、常數管理、消除魔法數字、大規模統一化、重構陷阱防護、作用域迴歸、以及非程式碼的重構——這些都是從同一個真實專案提煉的。現在把時間線拉開,看看這些技術在三個階段中如何逐步應用,以及過程中犯了哪些錯。

起點:超過 30 個 Hook 各自為政

v0.28.0 之前的 Hook 系統是「有機生長」的典型案例。32 個 Hook 各自獨立開發,沒有共用程式庫、沒有統一風格、沒有測試。

具體問題:

問題嚴重度量化
單檔過大task-dispatch 達 858 行
函式重複run_git_command 等函式在多個檔案中複製貼上
配置硬編碼代理人清單散落在程式碼各處
魔法數字line[9:] 這類寫法隨處可見
無測試任何修改都是盲改

這些問題不是一天造成的。每次新增 Hook 時,最快的做法就是從現有 Hook 複製一份再改。沒人想著「先建共用模組」,因為每次都只是「加一個小功能」。第一個 Hook 50 行,第二個 80 行,到第十個 Hook 時,已經有三四份 run_git_command 的副本了。但每次都覺得「下次再整理」。

程式碼壞味道識別 中教的 grep 方法掃描一次:

1grep -rh "^def " .claude/hooks/*.py | sort | uniq -c | sort -rn | head -5

掃描後會發現同樣的函式定義散落在多個檔案中。run_git_command 出現在 2 個以上的 Hook 裡,get_current_branchread_yaml_file 等也有各自的副本。如果修復 run_git_command 的一個 bug,你需要同時改每個有副本的檔案,而且不能漏掉任何一個。

累積到 32 個 Hook 時,技術債務已經大到無法忽視。

第一階段:v0.28.0 結構性重構

第一階段的目標很明確:消除重複、建立結構。我們把工作拆成四個 Wave,每個 Wave 有獨立的交付物和驗證點。

Wave 1:建立共用程式庫

先不動任何 Hook 檔案。第一步是把散落各處的重複邏輯抽取到獨立模組,並為每個模組寫測試。

建立的模組:

模組職責對應的壞味道
config_loader讀取 YAML 配置ARCH-001 硬編碼配置
git_utils封裝 Git 命令IMP-001 重複程式碼
hook_io統一 Hook I/O 處理IMP-001 重複程式碼
hook_logging統一日誌設定IMP-001 重複程式碼

為什麼先建程式庫而不是先改 Hook?因為如果直接改 Hook,會遇到雞生蛋問題——Hook A 需要共用函式,但共用函式還沒建立。先建好程式庫並通過測試,後續每改一個 Hook 都有安全網。

建立模組時的關鍵決策:介面設計先於實作。我們先定義每個模組的公開函式簽名,寫測試驗證這些簽名的行為,最後才把各 Hook 中的重複邏輯搬進來。這確保了模組的介面是「為使用者設計」的,而不是「照搬原始碼」的。

這個順序的思考方式在 DRY 原則與共用程式庫 中有詳細說明。

Wave 2:配置分離

把 task-dispatch 中的硬編碼清單抽到 YAML 檔案:

1# agents.yaml(節錄)
2agents:
3  incident-responder:
4    triggers: ["test failed", "compile error", "runtime error"]
5    priority: 1
6  system-analyst:
7    triggers: ["架構", "設計", "需求"]
8    priority: 2

這一步的行數縮減最明顯。task-dispatch 中原本有大量的 if-elif 鏈在比對代理人名稱和觸發條件,類似這樣:

1# 修改前:60+ 行的 if-elif 鏈
2if "test failed" in message or "compile error" in message:
3    agent = "incident-responder"
4elif "架構" in message or "設計" in message:
5    agent = "system-analyst"
6elif "安全" in message or "auth" in message:
7    agent = "security-reviewer"
8# ... 還有 20 幾個 elif

全部變成配置查表後,程式碼只剩下查表邏輯本身:

1# 修改後:配置驅動
2agents = config_loader.load("agents.yaml")
3for agent_name, config in agents.items():
4    if any(trigger in message for trigger in config["triggers"]):
5        return agent_name

詳細的抽取過程在配置分離與常數管理中說明。

Wave 3:逐檔重構

有了共用程式庫和配置檔,開始逐一重構 Hook 檔案。策略是:

  1. 選擇一個 Hook 檔案
  2. from lib.xxx import yyy 替換重複程式碼
  3. 跑測試確認行為不變
  4. 確認通過後繼續下一個檔案

逐檔處理的好處是認知負擔可控——每次只需要理解一個 Hook 的邏輯,不需要同時在腦中處理所有修改。即使最後在同一個 commit 中提交,工作過程中仍然是一個一個檔案獨立驗證的。這就是 Wave 作為安全網的具體體現。

Wave 4:驗證與收尾

28 個單元測試全部通過。主要檔案的變化:

檔案重構前重構後縮減
task-dispatch-readiness-check.py858 行296 行-65%
branch-verify-hook.py238 行109 行-54%
branch-status-reminder.py167 行103 行-38%

第一階段成果

指標數值
消除重複程式碼約 415 行
新增共用模組4 個
新增單元測試28 個
建立 Error Patterns3 個(ARCH-001、IMP-001、IMP-002)

第一階段解決了最顯眼的問題:重複和膨脹。但還有更深層的問題藏在下面。

第二階段:v0.31.0 品質深化

第一階段建立了結構,但結構內部的品質仍然參差不齊。v0.31.0 的四個連續 Wave 處理的是「統一風格」這個看似簡單但實際上充滿陷阱的任務。

W22:統一日誌格式

Hook 的日誌格式不一致。有的用 print,有的用 logging,有的用自訂格式。同樣是輸出一行日誌,你可能看到三種寫法:

1# 風格 A:直接 print
2print(f"[{hook_name}] Processing ticket {tid}")
3
4# 風格 B:logging 模組
5logging.info(f"Processing ticket {tid}")
6
7# 風格 C:自訂 logger
8logger = setup_hook_logging(hook_name)
9logger.info(f"Processing ticket {tid}")

W22 統一為風格 C,讓所有 Hook 的日誌都通過 hook_logging 模組。這個 Wave 相對順利,因為只涉及輸出格式的統一,不改變程式邏輯。日誌行為改變不影響 Hook 的核心功能。

W23:統一錯誤訊息

把散落在各 Hook 中的硬編碼錯誤訊息提取到集中的 messages 模組。這對應的是 配置分離與常數管理 中「禁止硬編碼字串」的原則。

1# 修改前:訊息散落各處,同一個概念有不同的說法
2print("Error: ticket not found")       # Hook A
3print("找不到 ticket")                  # Hook B
4print("Ticket does not exist")          # Hook C
5
6# 修改後:集中管理,一個概念一種說法
7from lib.messages import HookMessages
8print(HookMessages.TICKET_NOT_FOUND)

集中管理的好處不只是一致性。如果需要把所有訊息改成中文,只需要改 messages 模組,不需要搜尋散落在幾十個檔案中的字串。

W24:統一 Logger 初始化風格——IMP-003 事故

這是第二階段最慘痛的一課。

目標很簡單:把所有 Hook 的 logger = setup_hook_logging(...) 從模組級移到 main() 內部。理由是 logger 不該在模組被 import 時就建立——這是 Python 社群的通用最佳實踐。

結果:7 個 Hook 靜默失敗,41 個函式受影響,至少 2 個 session 沒人發現。

根本原因是作用域變更logger 從全域變數變成 main() 的區域變數後,其他函式無法存取它。Python 的 LEGB 規則決定了 main() 的區域變數對同級的其他函式是不可見的。

更危險的是,run_hook_safely 的頂層例外處理把 NameError 吞掉了——它捕獲所有 Exception,只寫入檔案日誌而不輸出到 stderr 或 stdout。於是使用者端完全看不到任何異常。

這個事故的完整分析在作用域迴歸案例研究中。

IMP-003 帶來兩個直接改善:

  1. 作用域變更檢查清單:任何涉及變數作用域變更的重構,都必須先用 AST 分析列出所有引用,逐一確認每個函式的存取方式
  2. stderr 輸出_log_exception 在寫入檔案日誌後,額外輸出到 stderr,確保 Hook 失敗對使用者可見——再也不會有「靜默失敗」的情況

W25:修復連鎖問題——IMP-005

W22 的模組遷移留下了另一個隱患。把 common_functions.pyhooks/ 遷移到 hooks/lib/ 時,部分 Hook 的 import 路徑沒有同步更新,導致 ModuleNotFoundError。這就是 IMP-005(模組遷移後 Import 路徑未同步更新),影響了 5 個 Hook。

修正流程本身不複雜:更新 import 路徑就好。但問題是遷移時沒有系統性地掃描所有引用——只改了「知道有引用的」檔案,漏掉了幾個不常觸發的 Hook。

這裡學到的教訓是:批量修正必須機械化。應該用 grep 或 AST 分析列出所有引用點,再逐一修改並驗證,確認沒有遺漏。手動作業的錯誤率和修改數量成正比。

第二階段成果

指標數值
統一的風格規範日誌格式、錯誤訊息、初始化方式
新增 Error Patterns2 個(IMP-003、IMP-005)
受影響的事故7 Hook 靜默失敗(IMP-003)
新增防護機制stderr 輸出、作用域檢查清單、AST 驗證

第二階段的教訓比第一階段更有價值。結構性重構(消除重複、建立模組)相對直接,風險可控。但風格統一涉及的是「改變現有能運作的程式碼」,任何疏忽都可能引入迴歸。

第三階段:系統級改善

前兩個階段解決了程式碼層面的問題。第三階段的視角拉高到「系統如何自我保護」。以下按主題分組,部分改善與第二階段穿插進行。

W9:Progressive Disclosure 精簡

隨著規則、方法論、指南越寫越多,文件系統本身的認知負擔也在增加。一份「並行派發指南」原本 200 行,因為不斷補充場景表、案例、FAQ,膨脹到 600 行。讀者只想知道「怎麼判斷能不能並行」,卻被淹沒在細節中。

W9 做了一次系統性的文件瘦身:

  • 主文件只保留核心規則和決策邏輯(判斷標準、流程圖、檢查清單)
  • 詳細說明、範例、模板移到 references/ 子目錄
  • 每份主文件的 token 數量縮減 20-40%
  • 需要深入了解時,透過連結跳到 references

這不是程式碼重構,但思考方式完全一樣:識別膨脹 → 分析哪些是核心哪些是細節 → 職責分離 → 驗證可讀性。重構的對象不只是程式碼,任何隨時間膨脹的結構化資訊都適用同樣的方法。

W28:一致性審查

對所有 Hook、規則、方法論進行一致性審查。檢查項目包括:

  • 命名是否遵循統一規範(例如 Hook 檔名的 kebab-case)
  • 錯誤處理是否都通過 run_hook_safely
  • 日誌格式是否統一(W22 的成果是否被維持)
  • 配置是否都從 YAML 讀取(W2 的成果是否被維持)
  • 新 Hook 是否使用共用程式庫(W1 的成果是否被維持)

審查的結果是發現了幾個遺漏:v0.28.0 之後新建的 Hook 有部分沒有使用共用程式庫,而是又開始「複製貼上」。開發者說:「我只是從隔壁 Hook 複製了幾行,沒必要引入整個模組。」這正是 v0.28.0 之前所有問題的起點。

這說明制度化比一次性重構更重要。如果沒有持續的品質檢查,程式碼會自然退化回混亂狀態。一致性審查不是做一次就結束,它需要成為定期的衛生檢查。

量化總結:階段對比

維度第一階段 (v0.28.0)第二階段 (v0.31.0)第三階段 (系統級)
目標消除重複、建立結構統一風格、深化品質系統自我保護
方法抽取模組、配置分離風格統一、訊息集中文件精簡、一致性審查
工作量Wave 1-4W22-W25W9、W28
主要產出4 模組、28 測試統一風格、2 Error Patterns文件瘦身、檢查機制
事故IMP-003(7 Hook 靜默失敗)
認知負擔變化大幅降低(檔案縮減 38-65%)中度降低(風格一致)間接降低(文件可讀性)
風險等級低(新建模組不影響現有)高(修改現有能運作的程式碼)低(不涉及程式邏輯)

一個重要觀察:第二階段的風險最高。第一階段是「加法」(新增模組),第三階段是「非程式碼」(文件調整),而第二階段是「改動現有程式碼」。這正是 IMP-003 發生的背景。

重構的風險不取決於修改的「量」,而取決於修改的「性質」。W24 的每個修改都很小(移動一行 logger = ...),但每個修改都觸及了 Python 作用域這個容易被忽略的基礎機制。

教訓

重構是持續過程,不是一次性事件

v0.28.0 做完時,我們以為重構結束了。結果 v0.31.0 又花了四個 Wave 處理品質問題,後面還有系統級的調整。

程式碼會自然退化。每次新增功能、修復 bug、趕進度,都可能引入新的技術債務。重構不是「做完就好」的專案,而是持續進行的衛生習慣——就像每天刷牙,不是做一次根管治療就可以不刷了。

Error Patterns 是知識累積

五個 Error Patterns(ARCH-001、IMP-001、IMP-002、IMP-003、IMP-005)不只是問題記錄。它們是團隊的「免疫記憶」:

  • ARCH-001(硬編碼配置)→ 以後新增配置時,自動想到用 YAML
  • IMP-001(重複程式碼)→ 發現重複時,自動想到抽取模組
  • IMP-002(魔法數字)→ 看到裸數字時,自動想到具名常數
  • IMP-003(作用域迴歸)→ 移動變數定義時,自動想到影響範圍分析
  • IMP-005(模組遷移後 Import 路徑未同步更新)→ 搬移模組時,自動想到掃描所有引用點

每個 Error Pattern 都有明確的結構:觸發條件、根本原因、檢查清單、防護措施。新成員不需要親身經歷這些事故,讀文件就能獲得防護。這比「口耳相傳」可靠得多——口頭經驗會隨著人員流動而消失,文件化的 Error Pattern 是永久的。

Wave 是安全網

把大型重構拆成 Wave 的好處:

  1. 獨立驗證:每個 Wave 結束時都跑完整測試,確認沒改壞東西
  2. 可回滾:如果 Wave 3 出問題,Wave 1 和 2 的成果不受影響
  3. 認知管理:每次只需要理解一個 Wave 的範圍,不需要在腦中同時處理所有修改
  4. 進度可見:每完成一個 Wave 就有具體的交付物,而不是「重構了三天但還沒完成」

Wave 2(配置分離)如果和 Wave 3(逐檔重構)合併,認知負擔會超過上限——需要同時思考「配置怎麼設計」和「Hook 怎麼改」。拆開後每次只需要想一件事。

反過來說,Wave 也防止了「過度設計」。如果一開始就試圖設計完美的共用程式庫,可能會花太多時間在抽象設計上。Wave 1 先建立「夠用」的模組,Wave 3 在實際使用時再調整介面。實踐中的回饋比預先設計更可靠。

風格統一比結構重構危險

第一階段(結構重構)幾乎沒有事故。第二階段(風格統一)出了 IMP-003。

原因是:結構重構主要是「加法」——建立新模組、新測試。現有程式碼的修改量小,改壞的機率低。而風格統一是在「改動能運作的程式碼」,每一行修改都可能引入迴歸。

如果重來,W24 的做法會改成:

  1. 先寫自動化腳本做 AST 分析,列出每個 Hook 的所有 logger 引用關係
  2. 用腳本自動修改函式簽名和呼叫端,而不是手動逐檔改
  3. 修改後立刻對每個 Hook 做隔離測試,不是改完全部再測
  4. 每改完一個 Hook 就提交一次,而不是改完全部再提交

這些改善措施的共同主題是:縮小每次改動的影響範圍。一次改一個 Hook 比一次改 16 個 Hook 安全得多。

章節知識地圖

本模組各章對應到重構過程的哪個環節:

章節對應的壞味道對應的階段核心技能
重構的動機與策略全部起點(為什麼重構)認知負擔量化、階段分解
程式碼壞味道識別所有起點(識別問題)grep 分析、5 Why
DRY 原則與共用程式庫IMP-001第一階段 W1模組抽取、介面設計
配置分離與常數管理IMP-002, ARCH-001第一階段 W2 + 第二階段 W23三種硬編碼的系統性消除
大規模統一化重構IMP-001, ARCH-001第二階段 W22-W24三階段統一化、漸進式重構
重構陷阱與防護IMP-003, IMP-005第二階段 W24-W25部分更新防護、AST 驗證
作用域迴歸案例研究IMP-003第二階段 W24AST 分析、作用域規則
非程式碼的重構文件壞味道第三階段 W9Progressive Disclosure、文件精簡
本章(完整案例回顧)全部全部系統性思考、Wave 規劃

每一章都可以獨立閱讀,但它們來自同一個持續演進的真實專案。單獨學會「怎麼消除魔法數字」是基礎能力;理解「什麼時候該做、以什麼順序做、做的時候可能出什麼事故」才是重構的完整技能。

重構前後的程式碼對比

用一段典型的程式碼說明三個階段的累積效果:

 1# === 起點:v0.28.0 之前 ===
 2# 直接使用 subprocess,硬編碼分支名稱,魔法數字
 3result = subprocess.run(
 4    ["git", "branch", "--show-current"],
 5    capture_output=True, text=True
 6)
 7branch = result.stdout.strip()
 8if branch in ["main", "master", "develop"]:
 9    print("Error: protected branch")
10    sys.exit(1)
11
12# === 第一階段後:v0.28.0 ===
13# 使用共用模組,但訊息仍硬編碼
14from lib.git_utils import get_current_branch, is_protected_branch
15
16branch = get_current_branch()
17if is_protected_branch(branch):
18    print("Error: protected branch")
19    sys.exit(1)
20
21# === 第二階段後:v0.31.0 ===
22# 訊息集中管理,logger 正確初始化
23from lib.git_utils import get_current_branch, is_protected_branch
24from lib.messages import BranchMessages
25
26def check_branch(logger):
27    branch = get_current_branch()
28    if is_protected_branch(branch):
29        logger.warning(BranchMessages.PROTECTED_BRANCH)
30        return EXIT_ERROR
31    return EXIT_SUCCESS
32
33def main():
34    logger = setup_hook_logging("branch-verify")
35    return check_branch(logger)

認知負擔的變化:

版本需要同時理解的概念認知負擔指數
起點subprocess API、Git 命令語法、分支名稱列表、退出碼8
第一階段後共用函式名稱、退出碼4
第二階段後共用函式名稱、訊息常數名稱3

每一階段都在降低閱讀者需要同時記住的東西。這就是 序章的認知負擔理論 在實踐中的應用。

小結

回顧整個過程,重構的節奏是:

  1. 先解決最痛的問題(第一階段:重複和膨脹)
  2. 再提升內部品質(第二階段:風格和一致性)
  3. 最後建立保護機制(第三階段:系統級防護)

每個階段都需要前一個階段的基礎。沒有共用模組就無法統一風格,沒有統一風格就無法做一致性審查。

而貫穿三個階段的不變原則只有一個:這段程式碼讓讀者需要同時記住多少東西? 如果太多,就需要重構。不管是 858 行的單檔、散落各處的錯誤訊息、還是膨脹到 600 行的文件——認知負擔就是重構的指北針。


上一章:非程式碼的重構

回到模組總覽:模組七:重構實戰