完整案例回顧
本章是模組七的總結。前面九章從動機判斷開始,依序教了壞味道識別、配置分離、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_branch、read_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 檔案。策略是:
- 選擇一個 Hook 檔案
- 用
from lib.xxx import yyy替換重複程式碼 - 跑測試確認行為不變
- 確認通過後繼續下一個檔案
逐檔處理的好處是認知負擔可控——每次只需要理解一個 Hook 的邏輯,不需要同時在腦中處理所有修改。即使最後在同一個 commit 中提交,工作過程中仍然是一個一個檔案獨立驗證的。這就是 Wave 作為安全網的具體體現。
Wave 4:驗證與收尾
28 個單元測試全部通過。主要檔案的變化:
| 檔案 | 重構前 | 重構後 | 縮減 |
|---|---|---|---|
| task-dispatch-readiness-check.py | 858 行 | 296 行 | -65% |
| branch-verify-hook.py | 238 行 | 109 行 | -54% |
| branch-status-reminder.py | 167 行 | 103 行 | -38% |
第一階段成果
| 指標 | 數值 |
|---|---|
| 消除重複程式碼 | 約 415 行 |
| 新增共用模組 | 4 個 |
| 新增單元測試 | 28 個 |
| 建立 Error Patterns | 3 個(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 帶來兩個直接改善:
- 作用域變更檢查清單:任何涉及變數作用域變更的重構,都必須先用 AST 分析列出所有引用,逐一確認每個函式的存取方式
- stderr 輸出:
_log_exception在寫入檔案日誌後,額外輸出到 stderr,確保 Hook 失敗對使用者可見——再也不會有「靜默失敗」的情況
W25:修復連鎖問題——IMP-005
W22 的模組遷移留下了另一個隱患。把 common_functions.py 從 hooks/ 遷移到 hooks/lib/ 時,部分 Hook 的 import 路徑沒有同步更新,導致 ModuleNotFoundError。這就是 IMP-005(模組遷移後 Import 路徑未同步更新),影響了 5 個 Hook。
修正流程本身不複雜:更新 import 路徑就好。但問題是遷移時沒有系統性地掃描所有引用——只改了「知道有引用的」檔案,漏掉了幾個不常觸發的 Hook。
這裡學到的教訓是:批量修正必須機械化。應該用 grep 或 AST 分析列出所有引用點,再逐一修改並驗證,確認沒有遺漏。手動作業的錯誤率和修改數量成正比。
第二階段成果
| 指標 | 數值 |
|---|---|
| 統一的風格規範 | 日誌格式、錯誤訊息、初始化方式 |
| 新增 Error Patterns | 2 個(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-4 | W22-W25 | W9、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 的好處:
- 獨立驗證:每個 Wave 結束時都跑完整測試,確認沒改壞東西
- 可回滾:如果 Wave 3 出問題,Wave 1 和 2 的成果不受影響
- 認知管理:每次只需要理解一個 Wave 的範圍,不需要在腦中同時處理所有修改
- 進度可見:每完成一個 Wave 就有具體的交付物,而不是「重構了三天但還沒完成」
Wave 2(配置分離)如果和 Wave 3(逐檔重構)合併,認知負擔會超過上限——需要同時思考「配置怎麼設計」和「Hook 怎麼改」。拆開後每次只需要想一件事。
反過來說,Wave 也防止了「過度設計」。如果一開始就試圖設計完美的共用程式庫,可能會花太多時間在抽象設計上。Wave 1 先建立「夠用」的模組,Wave 3 在實際使用時再調整介面。實踐中的回饋比預先設計更可靠。
風格統一比結構重構危險
第一階段(結構重構)幾乎沒有事故。第二階段(風格統一)出了 IMP-003。
原因是:結構重構主要是「加法」——建立新模組、新測試。現有程式碼的修改量小,改壞的機率低。而風格統一是在「改動能運作的程式碼」,每一行修改都可能引入迴歸。
如果重來,W24 的做法會改成:
- 先寫自動化腳本做 AST 分析,列出每個 Hook 的所有 logger 引用關係
- 用腳本自動修改函式簽名和呼叫端,而不是手動逐檔改
- 修改後立刻對每個 Hook 做隔離測試,不是改完全部再測
- 每改完一個 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 | 第二階段 W24 | AST 分析、作用域規則 |
| 非程式碼的重構 | 文件壞味道 | 第三階段 W9 | Progressive 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 |
每一階段都在降低閱讀者需要同時記住的東西。這就是 序章的認知負擔理論 在實踐中的應用。
小結
回顧整個過程,重構的節奏是:
- 先解決最痛的問題(第一階段:重複和膨脹)
- 再提升內部品質(第二階段:風格和一致性)
- 最後建立保護機制(第三階段:系統級防護)
每個階段都需要前一個階段的基礎。沒有共用模組就無法統一風格,沒有統一風格就無法做一致性審查。
而貫穿三個階段的不變原則只有一個:這段程式碼讓讀者需要同時記住多少東西? 如果太多,就需要重構。不管是 858 行的單檔、散落各處的錯誤訊息、還是膨脹到 600 行的文件——認知負擔就是重構的指北針。
上一章:非程式碼的重構
回到模組總覽:模組七:重構實戰