Claude Code 做 safe refactor:先 characterization test、後 mechanical move、再核對等價性
Claude Code 做 safe refactor 嘅三步流程:先 characterization test 釘住現有行為、再 mechanical move 唔改邏輯、最後核對等價性。教你避開 AI 直接重寫撞爛 edge case 嘅死位。
情境
你接手一個 5 年舊 codebase。其中一個 file —— orderProcessor.php 又或者 order-service.js —— 800 行、4 層 nested if、3 個 author 已離職、無 test 但跑咗 5 年生產環境。
老闆叫你加新 feature。你開個 file 睇咗 10 分鐘決定:呢個唔 refactor 真係加唔到嘢。
你開 Claude Code,順口講:
Refactor 呢個 file,抽出啲 helper、改靚啲命名、令佢易讀。
Claude 跑 5 分鐘出 250 行新 code。你睇落幾靚仔。合併落 staging。
然後 QA 嗌救命 —— 有個 5 年前 patch 落去嘅怪 edge case(負數 quantity 自動轉做退款流程)冇咗。Claude 重寫嘅時候諗都無諗過呢個情況存在,因為呢個行為從來無人寫低。
Refactor 死位永遠係:你以為自己保留咗原有行為,其實打爛咗 5 年累積落嚟嘅隱藏合約。
呢篇教你用 Claude Code 嘅三步 safe refactor 流程。慢半個鐘,但 production 唔會嗌救命。
跟住做
1. 劃清 refactor 範圍
開 Claude Code 之前,先用文字定義清楚邊個 module、邊個 function 要動,唔好「順手執埋隔離」。範圍失控係 refactor 撞車嘅頭號原因。
我要 refactor 呢個 file:src/services/orderProcessor.ts。範圍紀律: 只動呢一個 file 唔好順手改其他 file 嘅 import path(如果改唔到 callsite 我哋第三步先做) 唔好 touch tests 同 config 唔好 upgrade 任何 dependency 請先讀晒成個 file,輸出: 一個「public surface」清單:邊啲 function / class 係 export 出去、外面點 call 一個「internal behavior」清單:包括所有 if/else branch、early return、throw、副作用(DB write、log、external API) 一個「可疑 edge case」清單:你睇 code 覺得「呢度寫得怪、可能係特意 patch」嘅地方 唔好寫任何 code。淨係輸出呢三張清單。
第一步唔出 code —— 出清單。呢張清單就係下一步 characterization test 嘅原材料。
2. Characterization test:釘死現有行為
Characterization test 嘅定義:唔係驗證 code「應該」做乜(你唔知),而係記錄 code「家陣」做緊乜。包括啲怪嘢、似 bug 嘅行為、5 年前嘅 patch。
基於第一步個「internal behavior」+「可疑 edge case」清單,幫我寫一套 characterization test:要求: 用呢個 project 已經有嘅 test framework(Jest / Vitest / PHPUnit,你自己睇 package.json / composer.json) 唔可以 mock 太多 —— 真實 input、真實 output、只 mock 外部 IO(DB / HTTP) 每個 test case 對應一個 observable behavior,test name 用英文描述行為,例如 returns refund flow when quantity is negative 對於「可疑 edge case」嗰啲,每個都要一個獨立 test —— 即使你覺得「呢個係 bug」都要 釘選 住佢,refactor 之後再決定改唔改 跑一次確認全部 pass(如果 fail 即係你估錯 behavior,要返去再讀 code) ⚠️ 紀律:呢一步完全唔可以 touch production code。淨係加 test file。寫完跑 npm test / pnpm test 確認全綠先收工。
呢一步通常出 15-40 個 test。睇落好多,但每個都係將「冇人寫低嘅口頭合約」變成「跑得起嘅安全網」。
3. Mechanical refactor:只搬唔改
呢一步先郁 production code。但有條鐵律:Claude 只可以做機械式 transformation —— rename、extract、move、inline、reorder —— 唔可以改邏輯。
而家可以動 orderProcessor.ts。但係有鐵律:✅ 允許嘅 transformation:
呢度個關鍵字係 mechanical。機械式 transformation 嘅特性係:理論上可以由 IDE 自動做,行為數學上等價。Claude 一旦開始「改良邏輯」,你就要叫停。
4. 核對等價性 + 拆 commit 節奏
跑曬 mechanical refactor 之後,仲要做最後驗證 —— 因為 test suite 唔可能 100% 覆蓋 production 入面真實 input 嘅分布。
幫我做最後驗證: 跑全套 test,confirm 全綠 + coverage 報告(focus 喺 orderProcessor.ts) 對住 git 嘅 pre-refactor commit,揀 5 個「realistic input sample」—— 包括兩個正常 case、一個 edge case、一個錯誤 input、一個我哋第一步標咗「可疑」嘅 case 對每個 sample,用 git stash 切換 pre/post refactor 兩個版本,分別跑一次,逐 byte 對比 output(包括 log line、DB write payload、return value) 如果有任何差異,標返出嚟 —— 即使「我哋覺得新 behavior 更正確」都要報告 完成後幫我整理 commit: Commit 1:「test: add characterization tests for orderProcessor」(第二步成果) Commit 2:「refactor: extract helpers from orderProcessor (no behavior change)」(第三步成果) 兩個 commit 之間任何時間 revert commit 2 都唔影響 commit 1
兩個 commit 分開係紀律核心。Commit 1 係永遠有價值嘅資產(即使 refactor 失敗都留低個 test 安全網)。Commit 2 係可棄置嘅嘗試。
變化
變化 1:Rename refactor(大型符號搬遷)
成個 module 改名、或者一個 class 改名牽連幾十個 file。呢個情況試圖一次過手做必死,但又最適合機械式咁做。
我要將 OrderProcessor rename 做 OrderService,連 file name、class name、所有 import、所有 mention。紀律: 先 git grep -n "OrderProcessor" 出曬所有出現位置,列張清單畀我睇過 我確認清單之後,分三批做:file rename → declaration rename → callsite rename 每批之後跑 typecheck + test 唔好順手改任何其他嘢 —— 如果你發現「順手執埋會好啲」,加入 TODO list,呢次 PR 唔做
呢類純 rename 嘅 PR 最理想,因為 reviewer 可以一句「all mechanical」就批准。
變化 2:Extract method(拆大 function)
200 行嘅 function,要拆做 5 個 helper。
processOrder() 而家 220 行。我要拆做幾個 helper:要求: 你先標返 220 行入面邊幾段 logical block(例如 validation / pricing / inventory / persistence / notification) 每段建議一個 helper 名(純 descriptive、唔諗 abstraction) 由最尾果段(side effect 最少)開始 extract —— 因為尾段最 isolated 每 extract 一個 helper,跑 test,commit 一次 全部 extract 完之後,processOrder() 應該係 5-7 行 sequential call 唔好試引入新 abstraction(class、interface、空泛)。淨係搬位置。
關鍵:extract 嘅次序由尾向頭。尾段 side effect 少、最 isolated、最易抽離。
變化 3:API change(callers 都要跟住改)
最危險。Function signature 改 = 所有 callsite 都要改。
我要將 getUser(id) 改成 getUser({ id, includeDeleted })。紀律:
先唔好改 signature —— 加一個新 function getUserV2({ id, includeDeleted }),旁邊並存
一個 callsite 一個 callsite 遷移,每次 migrate 完跑 test + commit
全部 callsite 遷曬之後,先刪除舊 getUser,再將 getUserV2 rename 返做 getUser
中途如果有 callsite 我哋唔肯定點處理(例如外部 API 調用),標 TODO,唔好硬遷
呢個係 expand-migrate-contract 模式。比起「一次過改 signature 然後 fix all callsites」安全好多 —— 因為每個中途 commit 都係跑得起嘅狀態。Expand-migrate-contract 係 safe refactor 嘅靈魂規律。中間每一步 main 都部署得,唔需要長命 branch。
拆解:點解 work,同邊度會仆街
跟到上面就已經安全用得。下面呢段係畀**想由「test 全綠收工」做到「PR 唔會喺三個月後反咬你」**嘅人——初學者可以跳過,唔影響你跟住做。
Safe refactor 最唔老實嘅地方係:test 全綠唔等於 behavior 真係冇變。test suite 係你寫嘅,你唔知道嘅 edge case,test 入面一樣冇。呢個流程實際會喺呢幾個位仆街,你要預咗:
1. Characterization test 釘錯咗 behavior(test 自己就係錯) 你叫 Claude「跑一次確認全部 pass」——但如果個 test 一開始就假設錯咗現有行為,佢會寫一個將個假設變成綠燈嘅 test,然後你以為釘死咗,其實釘咗個幻覺。
- 會出事:refactor 之後 test 全綠,但 production 真係變咗 behavior,因為你個安全網本身就漏底。
- 點救:第二步寫完 test,喺未 refactor 之前先用真實 production input 跑一次,逐個 assert 同你肉眼睇 code 嘅預期對一對;可疑 case 尤其要人手核實,唔好淨係信「綠燈」。
2. 「機械式」transformation 其實改咗 behavior
Extract function、move 次序、early return 取代 nested if——呢啲喺有 side effect 或者 short-circuit 嘅 code 入面,唔係真係等價。例如將一段 && 條件拆出 helper,evaluation 次序同 early exit 可能變咗。
- 會出事:Claude 真心覺得自己只係「搬位」,但 short-circuit、exception 傳播、log 次序已經唔同。
- 點救:堅持「每個 transformation 跑一次 test、變紅即 revert」嗰條鐵律,唔好攒住一次過做五個 transformation 先跑;越細粒度越易 bisect 邊步搞爛。
3. Test 唔覆蓋嘅 input 分布,先係 production 真正跑嘅嘢 第四步揀 5 個 sample 對 byte——但 production 一日跑幾萬條 input,你揀嗰 5 個唔代表真實分布。Claude 對「等價」嘅判斷只限於佢見過嘅 sample。
- 會出事:refactor 後上線,撞到一個你 sample 冇覆蓋嘅 input 組合先爆,而嗰時已經唔係你 review window。
- 點救:揀 sample 唔好靠估,如果有 production log,抽真實流量做 replay;高風險 module 考慮喺新舊兩版並行跑一段時間(shadow run)對數,確認零差異先拆走舊版。
4. Mass rename 漏咗 dynamic reference
變化 1 用 git grep 出曬 OrderProcessor——但 grep 捉唔到字串拼出嚟嘅引用:reflection、config 入面個 class name string、序列化資料、log 上落格式。
- 會出事:typecheck 同 test 全綠,但 runtime 一個
class_exists或者反序列化舊資料就 fail,而呢啲路徑通常冇 test。 - 點救:rename 唔好淨靠 grep symbol,要連 string literal、config、序列化過嘅舊資料一齊搜;改名後喺有真實資料嘅環境 smoke 一次,唔好淨喺 test 環境收貨。
5. 拆 commit 拆得靚,但 reviewer 一個 squash 就化為烏有 第四步特登將 commit 1(test)同 commit 2(refactor)分開,等可以獨立 revert——但合 PR 嗰陣一個 squash merge,兩個 commit 變一個,你嘅「test 永遠保留、refactor 可棄置」嘅結構喺 main 上面就冇咗。
- 會出事:之後想 revert 個 refactor,順手連 characterization test 都 revert 埋,安全網消失。
- 點救:呢類 PR 唔好 squash,用 merge commit 或者 rebase 保留兩個 commit;或者乾脆分兩個 PR,test 先入 main,refactor 後出,責任界線最清。
呢幾個位,就係「test 全綠收工」同「三個月後都唔會反咬你」之間嘅距離。
一個心態
Make the change easy, then make the easy change. —— Kent Beck
Refactor 嘅死位唔係技術,係心理。你睇住一段「明顯寫得屎」嘅 code,個衝動係即刻重寫。Claude Code 仲放大呢個衝動 —— 因為佢 5 分鐘真係出到 250 行新 code,個誘惑大過你抵擋到嘅意志力。
三步流程嘅核心唔係教 Claude 點 refactor,係強制你慢落嚟:
- 第一步(characterization test):強制你承認自己唔了解現有行為。寫 test 嘅過程會發現「咦原來呢度仲有個 fallback」、「呢個 throw 我以為冇觸發過,但 test 證明有」。
- 第二步(mechanical refactor):強制你將「改良」同「搬位」分開。機械式搬完之後,code 通常已經易讀咗一半,你會發覺「原來唔使改邏輯都得」。
- 第三步(核對等價性):強制你對等價性保持懷疑,唔好信「test 過 = 等價」。
Claude Code 喺 legacy refactor 真正幫到手嘅地方唔係速度 —— 係將「冇人寫低嘅口頭合約」轉換做跑得起嘅 test。一旦轉換完,5 年前嗰個離職同事個 patch,終於有人睇得明、改得郁。
下次再撞到 800 行嘅怪 file,唔好問 Claude「幫我 refactor 呢個」。問佢「幫我寫 characterization test 釘住現有行為」。一個禮拜之後你會發現,呢句嘢嘅回報高過你 3 年寫過嘅任何 prompt。
文中工具 · 連結
- Claude Code CLI· 付費
開發者用 — terminal 入面同 Claude pair coding
睇完想同 Claude 一齊行一次?
撳一撳,就將成段 tutor 指示(連埋成篇文嘅內容)抄入剪貼簿。 貼入 Claude.ai 或 Claude Desktop,佢會用廣東話帶你一步一步行, 每步問你填關鍵位,最後畀返一個專為你情況寫嘅 prompt 帶走。
- 打工仔 · 180 分鐘
Claude Code 處理舊 codebase:點樣接手一個 5 年舊項目
Claude Code 舊 codebase 接手指南:資深開發者接手一個 5 年舊項目。教你用 Claude Code 自動梳理架構、搵無用代碼 (dead code)、寫接手指南。
- 創作者 · 50 分鐘
Claude Code 行 TDD:你寫 test,Claude 寫 impl — pair-programming 模式真係 work
Claude Code TDD 工作流:你負責 test、Claude 負責 impl 嘅 pair programming 模式。red-green-refactor 具體 prompt + edge case ratchet + 邊類問題啱用 TDD。
- 創作者 · 30 分鐘
Claude Code Subagents:5 個 agent 並行同時做嘢(重構 + 測試 + 寫文檔)
睇個大 PR 順序做要 30 分鐘。用 subagent 並行:程式碼審查 + 測試覆蓋 + 保安掃描 + PR 描述,4 個 agent 一齊跑 6-8 分鐘搞掂。教你點設定,同邊類任務適合並行。