我的好朋友 Claude
第 114 期|Claude Code|創作者、打工仔|

Claude Code 做 safe refactor:先 characterization test、後 mechanical move、再核對等價性

Claude Code 做 safe refactor 嘅三步流程:先 characterization test 釘住現有行為、再 mechanical move 唔改邏輯、最後核對等價性。教你避開 AI 直接重寫撞爛 edge case 嘅死位。

難度 ★★★時間 60 分鐘用具 Claude Code CLI、你個 project + test framework
【編者撰】一個香港人

情境

你接手一個 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 撞車嘅頭號原因。

第一步 — 鎖死 scope
我要 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。

第二步 — 寫 characterization test
基於第一步個「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 —— 唔可以改邏輯

第三步 — Mechanical refactor only
而家可以動 orderProcessor.ts。但係有鐵律:✅ 允許嘅 transformation:

呢度個關鍵字係 mechanical。機械式 transformation 嘅特性係:理論上可以由 IDE 自動做,行為數學上等價。Claude 一旦開始「改良邏輯」,你就要叫停。

4. 核對等價性 + 拆 commit 節奏

跑曬 mechanical refactor 之後,仲要做最後驗證 —— 因為 test suite 唔可能 100% 覆蓋 production 入面真實 input 嘅分布。

第四步 — Equivalence verification
幫我做最後驗證:
跑全套 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。呢個情況試圖一次過手做必死,但又最適合機械式咁做。

變化 1 — Mass rename safe pattern
我要將 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。

變化 2 — Extract method ritual
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 都要改。

變化 3 — API change with caller migration
我要將 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,然後你以為釘死咗,其實釘咗個幻覺。

2. 「機械式」transformation 其實改咗 behavior Extract function、move 次序、early return 取代 nested if——呢啲喺有 side effect 或者 short-circuit 嘅 code 入面,唔係真係等價。例如將一段 && 條件拆出 helper,evaluation 次序同 early exit 可能變咗。

3. Test 唔覆蓋嘅 input 分布,先係 production 真正跑嘅嘢 第四步揀 5 個 sample 對 byte——但 production 一日跑幾萬條 input,你揀嗰 5 個唔代表真實分布。Claude 對「等價」嘅判斷只限於佢見過嘅 sample。

4. Mass rename 漏咗 dynamic reference 變化 1 用 git grep 出曬 OrderProcessor——但 grep 捉唔到字串拼出嚟嘅引用:reflection、config 入面個 class name string、序列化資料、log 上落格式。

5. 拆 commit 拆得靚,但 reviewer 一個 squash 就化為烏有 第四步特登將 commit 1(test)同 commit 2(refactor)分開,等可以獨立 revert——但合 PR 嗰陣一個 squash merge,兩個 commit 變一個,你嘅「test 永遠保留、refactor 可棄置」嘅結構喺 main 上面就冇咗。

呢幾個位,就係「test 全綠收工」同「三個月後都唔會反咬你」之間嘅距離。

一個心態

Make the change easy, then make the easy change. —— Kent Beck

Refactor 嘅死位唔係技術,係心理。你睇住一段「明顯寫得屎」嘅 code,個衝動係即刻重寫。Claude Code 仲放大呢個衝動 —— 因為佢 5 分鐘真係出到 250 行新 code,個誘惑大過你抵擋到嘅意志力。

三步流程嘅核心唔係教 Claude 點 refactor,係強制你慢落嚟

Claude Code 喺 legacy refactor 真正幫到手嘅地方唔係速度 —— 係將「冇人寫低嘅口頭合約」轉換做跑得起嘅 test。一旦轉換完,5 年前嗰個離職同事個 patch,終於有人睇得明、改得郁。

下次再撞到 800 行嘅怪 file,唔好問 Claude「幫我 refactor 呢個」。問佢「幫我寫 characterization test 釘住現有行為」。一個禮拜之後你會發現,呢句嘢嘅回報高過你 3 年寫過嘅任何 prompt。

文中工具 · 連結

  • 開發者用 — terminal 入面同 Claude pair coding

睇完想同 Claude 一齊行一次?

撳一撳,就將成段 tutor 指示(連埋成篇文嘅內容)抄入剪貼簿。 貼入 Claude.ai 或 Claude Desktop,佢會用廣東話帶你一步一步行, 每步問你填關鍵位,最後畀返一個專為你情況寫嘅 prompt 帶走。

下期預告 · 相關情境
訂閱本副刊

每週日早上,
一道新菜送到你 inbox。

一篇 use case、一個香港情境、一個跟得到嘅做法。 冇 sell course、冇話你「再唔學就會失業」。

訂閱通道執緊緊
newsletter service 仲未接通。想第一時間收到新文章——
直接 email 我哋寫一句「訂閱」就得。

Email 「訂閱」畀我