Refactor Pass
重構曾經始於一種感覺。程式碼很難處理:一個類別你需要滾動來找到新功能該放哪,一個函式你得記在腦中才能修改某個分支,一個功能意味著要造訪八個檔案。同樣的感覺告訴你哪些可以放著不管。你從未碰過的醜陋程式碼從未傷害過你,所以你就讓它保持原樣。
寫程式的 AI 代理感受不到這些,而開發人員大多也只閱讀 AI 寫的程式碼,所以壞味道會在不知不覺中堆積。AI 代理會毫不抱怨地為一個函式加上第三十個分支。而當被要求重構時,它會清理看起來不整潔的程式碼,包括那些根本沒人碰的程式碼。這個技能重新賦予你這兩半的感覺,透過三個可量化的指標:
- 你會在何處感覺到:熱力圖。 摩擦只在乎變更頻繁落腳的地方。跨多項研究顯示,檔案大小和變更頻率比任何單一壞味道更能預測維護問題。
- 你會感覺到什麼:壞味道。 經過驗證的壞味道目錄,每個都轉換為要統計什麼、何時不是壞味道,以及數十年實務為其建立的命名重構劇本:references/smells.md。
- 你感覺有多強烈:逐步模擬。 拿即將到來的下一次變更(無論是計畫中或可能發生的),列出它會觸及的所有內容。
然後,最終判定結合了「何處」與「什麼」:
| 有壞味道 | 無壞味道 | |
|---|---|---|
| 經常變更 | 重構 | 確認沒問題 |
| 很少變更 | 放著;最多加個「稍後」備註 | 什麼都不做 |
「確認沒問題」這個框和「重構」這個框同等重要。這是直覺中克制的那一半,並且報告會顯示出來。
兩種問題規模。「Pass」涵蓋整個儲存庫:步驟 1 到 7。關於單一區域的問題(「我們應該重構 X 嗎?」)則僅對該區域執行相同步驟。除非有多個項目,否則它會跳過檢查清單,而其答案可以是「否」。兩種情況下,請先執行步驟 1 的腳本並加上 --checklists 參數:如果它列出了先前 Pass 的檢查清單,請先閱讀在先前的 Pass 之後一節。
1. 何處:熱力圖
python3 <此技能目錄>/scripts/heatmap.py -C <儲存庫> <範圍...>
<此技能目錄> 是存放此 SKILL.md 的資料夾。如果 python3 不是指令(例如在 Windows 上),請改用 python 或 py。
- 範圍:原始碼資料夾和測試資料夾,例如
src tests或app,而不是 docs 或 assets。請閱讀專案的 README、目錄列表和任何代理指令(AGENTS.md、CLAUDE.md)來決定。測試檔案(透過名稱如Board.spec.ts、test_x.py、XTest.php... 或資料夾識別)應排除在表格和配對之外,並且當它們與多個檔案一起變更時,會單獨列出。使用--test GLOB加入其他測試名稱,使用--not-test GLOB排除看似測試檔案的檔案(熱力圖會列出它懷疑的檔案),使用--ignore GLOB排除產生的檔案。 - 視窗:預設為最近 200 個提交。當使用者指定起點時(「自 v2.0 起」、「自從我們加入計費功能後」、「最近三個月」),使用
--since取代:指定一個標籤、分支或提交,或一個日期。機器人提交、儲存庫在.git-blame-ignore-revs中列出的大量格式化提交,以及先前檢查清單中記錄的提交會自動排除。使用--exclude-commits排除其他會讓它們觸及的所有檔案看起來很熱門的提交,例如引入的供應商程式碼。 - 資料太少? 如果視窗內少於約 30 個提交,則計數無法區分熱門檔案和冷門檔案。請擴大視窗範圍,或者說明歷史紀錄太短而無法指出任何方向,僅根據壞味道來判斷。淺層克隆(熱力圖會這樣說)需要
git fetch --unshallow;請詢問使用者。
--help 說明每個欄位和選項。
解讀熱力圖
- 熱點檔案有許多提交且體積也大,因此列表會優先列出這些檔案:它按提交次數乘以大小排序。小型檔案有許多提交通常沒問題,例如每個功能都會編輯的設定檔。沒人碰的大型檔案則值得放著不管。
- 仍然很熱?
recent統計其在視窗較新的一半(一半的提交,而非一半的時間)中的提交次數,而last是其最新提交的日期。對照視窗的日期來解讀兩者:八個recent提交,全部集中在較新一半開始的那一天,代表這是一波已經冷卻的爆發。一個變小的檔案可能已經被處理過。 rel是相對於檔案大小的變更行數。相對變動率預測缺陷的能力遠優於絕對變動率。遠高於 1 表示該檔案在視窗內被重寫或增長。與 growth 一起解讀:一個只增長、一次又一次提交的檔案,就是僅加法增長。在視窗內新產生的檔案,其 growth 等於其大小。- 配對是在相同提交中不斷一起變更的檔案。一個群組是三個或更多彼此都配對的檔案,透過變更所有這些檔案的提交來計算;重疊的群組會一起列出為一個區域。
- 來自不同層級卻強烈配對的檔案,或跨層級的群組,通常意味著一個功能分散在它們之間(霰彈式修改)。
- 檢查每個配對的連結方式。一個匯入、一個呼叫,或某事物命名的契約(一個路由、一個 schema、控制器與其頁面之間的型別檔案),是可見的依賴:跨越邊界的一個功能。沒有此類連結而一起變更的檔案,例如規則的兩個副本,或手動保持同步的兩個列表,則具有隱藏的依賴,這是最強的耦合類型。
- 與許多其他檔案配對的檔案是每個變更都會通過的樞紐。
- 一個測試檔案除了自己的受測檔案外,還與多個檔案一起變更,表示它與這些檔案的內部實作緊密耦合,會阻礙重構,除非那些檔案是一個組,而測試只是跟隨它們。
- 如果許多提交因為檔案數量過多而無法進行配對(
--max-files),例如在進行 squash-merge 的儲存庫中,請提高該限制,或將配對解讀為部分資訊。
- 為什麼會變更。 閱讀前幾個檔案的提交訊息(
--show),並按功能分組。一個檔案因多個不相關的原因而變更,就是發散式變更,而在這裡比任何計數都更直接地表現出來。
2. 什麼:壞味道
- 針對程式碼庫本身(同一語言內)測量熱門檔案。
python3 <此技能目錄>/scripts/measure.py -C <儲存庫> <範圍...>會根據該語言的 70、80 和 90 百分位數,將每個檔案放入一個區間,而--show <檔案>則顯示其每個函式。「大」是 80 或 90 百分位數區間(這些區間也應用了目錄的最低門檻);70 百分位數區間則值得仔細查看。當它說百分位數傾向於某個檔案時,也要參考測量中的參考點,其中也說明了如何計算其餘項目(分支數、參數、內聚力),或如何使用專案的 linter。 - 自行閱讀前兩個或三個核心檔案。數字指出方向;閱讀才能決定。
- 審查其他熱點區域。如果你可以執行子代理,分配兩三個子代理各自負責一個區域(一個層級或一個目錄,這樣每個子代理會一次性讀取整個區域),並平行執行它們。根據 references/review-brief.md 為每個子代理簡報。如果沒有子代理,則自行逐一審查這些區域,並遵循相同的簡報。
- 使用其公認的名稱來命名每個壞味道(過長函式、霰彈式修改、重複開關……),並在計入之前,根據該條目的「不是壞味道的情況」進行檢查。名稱是存取其劇本的索引。
- 尋找缺失的概念。壞味道往往成群出現,而一個群組通常指向程式碼一直在使用但從未命名的某樣東西:一個狀態、一個類型的定義、一個設定的單一來源。請參閱壞味道一起出現。
- 檢查哪些測試觸及了熱門程式碼:請參閱未測試程式碼。
- 你注意到的錯誤作為 F 項目加入清單,但現在不修復。尚未重現的錯誤是一個懷疑:將其標題定為一個問題(「檢查:編輯跳過了週末規則」),並在備註中附上證據。其第一步是編寫一個會失敗的測試;如果沒有測試失敗,則跳過該項目,並附上你的發現。
3. 有多少:逐步模擬
這是最接近舊時感覺的東西。這也是讓一個項目具有說服力的關鍵。
- 找到接下來的變更。計畫中的變更優於推斷的變更,所以優先從使用者說即將進行的變更中獲取(「在我們開始第 7 塊之前」),然後從專案的計畫或路線圖中獲取(如果有的話),最後從提交訊息中獲取:最常出現的變更類型(「加入新的報表類型」、「加入一個設定」、「加入一個畫面」)。選取兩三種最常見的類型,而不是僅僅一種。
- 針對每種類型,取該類型的最新一個真實提交,並列出它觸及了哪些檔案(
git show --stat)。如果它比視窗更舊,照樣使用它,並提供其日期;在當今的程式碼上進行逐步模擬,可以看出哪些東西已經移動過。如果專案的文件或代理指令有針對該變更的 recipe,請計算其步驟數。一個五步驟的 recipe 用來加入一個東西,本身就是一種承認。 - 逐步模擬下一個變更,但不實際撰寫程式碼。列出你必須閱讀的每個檔案、你必須編輯的每個地方,以及你必須保持同步的每個副本。
- 寫下當今的計數,以及在你提議的重構之後的計數:「今天加入一個新的報表類型觸及 7 個檔案和 3 個開關,重構後只會觸及 1 個檔案」。
一個冷門檔案,如果計畫中的工作會落在它上面,就不再是冷門了:將其放入表格的頂行,因為下一次變更需要支付它帶來的任何成本。如果下一次變更在今天是便宜的,那麼無論壞味道看起來如何,它的成本都很低。
4. 檢查每個發現
審查者的發現是一個線索,而非事實,你自己的初步印象也是如此。在任何項目進入清單之前,請開啟引用的行號,重新執行計數,並刪除或修正不成立的項目。在報告中說明你檢查了什麼。一個錯誤的項目可能導致使用者進行一次耗費巨大的重構,遠超過花一分鐘檢查它的成本。
然後應用克制原則。被要求找出問題的審查者總是會找到一些東西,而修復每一個發現會留下針對不可能發生的情況的額外層級和測試。排除那些冷門的項目、那些被「不是壞味道的情況」所豁免的項目,以及那些逐步模擬顯示成本低廉的項目。
5. 排序
為每個項目分配一個穩定的 ID,因為使用者會透過它來引用項目(「開始 R4」),可能在後續的工作階段中。方括號內的名稱是項目在檢查清單(步驟 6)中的欄位;F、L 和 D 項目則在 note 中說明其欄位。
- R1, R2… 重構項目,按建議的順序排列。每個項目包含:
- 一個以結果命名缺失概念的標題(「命名結帳流程的狀態」,而不是「重構 CheckoutController」)(
title); - 按名稱區分的壞味道,附上數字和
file:line(smells); - 從步驟 3 得知,它所簡化的變更類型(
eases),以及該變更的成本(cost:today和after); - 計劃,即劇本中的步驟(
plan); - 檔案名稱、大小(S、M 或 L)和風險(低、中或高)(
files、size、risk); - 如何檢查行為沒有改變(
verify)。
- 一個以結果命名缺失概念的標題(「命名結帳流程的狀態」,而不是「重構 CheckoutController」)(
- F1… 過程中發現的小修正,每個都應該是一個獨立的提交。
- L1… 稍後處理:有壞味道但冷門,或者值得在下次修改附近程式碼時一併處理。
- D1… 由使用者決定:改變他們工作方式的項目,例如一個讓每個變更都變得昂貴的文件規範。列出權衡,而不是做出決定。
按報酬與風險排序。報酬是變更能減輕多少未來的工作:熱力圖和逐步模擬是證據。壞味道的優先級顯示哪些類型通常更重要。過長且複雜的程式碼,以及分散在檔案間的變更,最重要。Feature Envy 和 Middle Man 等耦合壞味道最不重要,並且通常是順便修正。
清單的順序應為:
- 阻塞項優先:與內部實作緊密耦合的測試(會讓後續每個步驟看起來像破壞了什麼),以及沒有測試覆蓋的程式碼(其項目從固定測試開始,或明確說明無法被檢查(步驟 7));
- 接下來是能移除複製貼上路徑的小項目;
- 大規模改動在後面,因為它們通常需要先建立一個縫隙;
- 任何你無法自行驗證的項目(聽起來、感覺起來或看起來如何)放在最後,因為需要使用者來檢查。
6. 撰寫檢查清單與報告
再次執行步驟 1 的指令,這次加上 --page .refactor-pass/<YYYY-MM-DD>.html,使用今天的日期。它會根據 assets/page.html 建立檢查清單,並將熱力圖寫入其 heatmap 區塊;再次執行時,它只會更新熱力圖,其他部分不變。你需要在它下方撰寫 pass 區塊,其中的 project、started、base 和 updated 已經填好:
summary:用兩三句話描述整體狀態;items:每個項目,包含步驟 5 的欄位、status(todo、doing、waiting等待使用者、done或skipped並在 outcome 中說明原因),以及commits和outcome(新項目為空);一個延續下來的項目則保留其from。每個項目以其 ID 和狀態作為第一行開頭,例如{"id": "R1", "status": "todo",,其後接其他欄位,如上方程式碼區塊上方的註解所示;不應使用 JSON pretty-printer 的排版方式,因為步驟 7 的狀態更新會編輯這一行;left_alone:每個包含what和why;checks:專案的檢查方式,來自其指令,或者如果沒有任何指令,則是你將手動檢查的內容;earlier:在先前的 Pass 之後,包含其checklist和results,每個針對該 Pass 完成的 refactor 包含:其id、title、eases和cost、接下來實際發生的change以及它實際actual觸及的內容(如果尚未發生則皆為空),以及一個note。否則為null;log:每個包含date和text,最新的在前。
每次編輯後,執行 python3 <此技能目錄>/scripts/check_page.py <頁面>,它會捕捉格式錯誤的括號、拼錯的欄位、重複的 ID 或沒有以單獨一行開頭的項目。如果你的環境可以發布或預覽 HTML 頁面,p
…
[因翻譯而截斷]