模組五|重構期的品質與節奏(Day 22–26)
我在一個團隊裡,把兩套活了六年的 Vue 2 後台管理系統升級到 Vue 3:一套給內部營運用(下面叫專案 A),一套給外部合作方用(專案 B)。這兩套原本是同一份程式碼被整包複製出去的,然後各自跑了六年。舊系統後來已經關機下線,這篇是升級那兩年裡的紀錄。
我把兩個新專案的改動類型分類統計,結果是這樣:
| 類型 | 專案 A | 專案 B |
|---|---|---|
| 修 bug | 48.9% | 44.5% |
| 新功能 | 38.3% | 43.3% |
| 重構 | 9.8% | 8.8% |
| 樣式與其他 | 3.0% | 3.4% |
修 bug 的比例跟做新功能差不多,甚至更高。
如果你把這張表拿給不熟悉重構的人看,他會得到一個很直接的結論:「這個團隊的品質有問題。」
我的看法相反。今天講為什麼。
「修 bug」這個標籤底下,其實混了三種完全不同的東西。
舊系統有一個行為,新系統搬過去的時候漏了。
這類最多,而且幾乎都不是「寫錯」,是「不知道有這件事」。
Day 16 舉過的例子:舊系統的查詢,日期留空預設查最近七天。這個行為沒有寫在任何規格裡,只存在於六年前那個人寫的一行程式碼裡。
重寫的人不會知道。他會照著「日期留空 = 不限制」的直覺寫,然後上線後被回報。
這類 bug 是遷移的必然成本,不是能力問題。
新框架、新元件庫、新的響應式機制(資料一變、畫面自動跟著更新的那套機制),都有自己的脾氣。
Day 8 講過的 reactive(Vue 3 建立響應式資料的兩種寫法之一)解構會失去響應性——欄位一旦被拆成獨立變數,就跟原本的資料斷了關係——就是這類,你以為它會動,它不會,而且不報錯。
這類 bug 有個特徵:同一種錯誤會在短時間內集中出現,然後隨著大家熟悉而消失。 它是學習曲線的具體形狀。
這一類最有價值,也最容易被誤會。
重構的時候,你被迫逐行讀過每一個檔案。而讀的過程中,你會看到一些「這樣寫應該會出事」的地方,然後去試,然後發現它真的會出事。
一個我們實際遇到的類型:某個數值計算在特定情況下型別會跑掉,導致金額顯示錯誤。這個問題在舊系統存在很久,但因為觸發條件很窄,沒有人回報過。
而它被修掉,不是因為有人回報,是因為有人在搬的時候讀到了那段程式碼。

第三類 bug 解釋了為什麼 fix 比例高不是壞事。
你搬家的時候,會把每一個櫃子、每一個抽屜都打開一次。
然後你會發現:床底下有一隻壞掉的插座、櫃子後面的牆壁有一片壁癌、廚房那個抽屜的軌道其實早就歪了。
這些問題不是搬家造成的。 它們一直都在,只是你平常不會把床搬開。
重構就是那個把床搬開的動作。
而這也帶出一個反直覺的判準:
重構期的修復數量,不是品質指標,是「你看得多仔細」的指標。
真正該擔心的是修復數量太少——那通常代表大家只是機械地把程式碼從左邊複製到右邊,沒有真的讀懂它。
道理講得通,不代表溝通會順利。
實際上你會遇到的場景是:
業務:「新系統上線之後,問題怎麼比以前多?」
而這個問題你不能用「重構是最好的 bug 掃描器」來回答,因為那聽起來像藉口。
我們後來學到的做法是把三類 bug 分開統計,然後這樣講:
第三類特別重要,因為它會改變對話的性質:從「你們把東西弄壞了」變成「原來我們一直有這個問題」。
而要能這樣講,前提是你在修的時候就要標記它是哪一類。事後才想分類,會分不出來。

分類之外,我們還做了一件事:針對最常出錯的模式,訂出寫法慣例。
第一類(搬遷失真)和第三類(舊有 bug)有一個共同的高發區:處理後端回傳的資料。
因為後端回什麼、有沒有可能是空的、巢狀結構長怎樣,這些在舊系統裡通常沒有型別、也沒有文件。
所以團隊訂了幾條很具體的慣例:
一、外部資料一律假設可能是空的
// 不要
dataSource.value = res.data.list
// 要
dataSource.value = res?.data?.list ?? []
這兩個符號在做的事:?. 叫可選鏈,中間任何一層是 null 或 undefined 就整條直接回傳 undefined,不會當場報錯;?? 叫空值合併,左邊真的什麼都沒有的時候,才換成右邊的預設值。
二、數值不要用真值判斷
// 不要 —— 0 是合法的金額,但會被當成「沒資料」
if (statistics.amount) { ... }
// 要
if (statistics.amount >= 0) { ... }
這條看起來很小,但它是一整類 bug 的來源:帳目金額剛好是 0 的時候,畫面上什麼都不顯示。 而測試資料通常不會剛好是 0。
三、空值顯示 --,不要留空白(Day 20 講過,落地了 397 次)
我去數了新系統:
| 寫法 | 出現次數 |
|---|---|
可選鏈 ?. |
6,046 |
空值合併 ?? |
1,348 |
| ` |
六千多次的可選鏈,平均每個檔案七次以上。
這代表「假設外部資料可能是空的」不是寫在文件裡的口號,它已經變成大家寫程式的預設動作。
而它能落地的原因,我認為跟 Day 20 那個 -- 一樣:規則夠小、而且違反時容易在程式碼審查(code review)被看到。
(附帶一提,「數值用 >= 0 判斷」只出現了 31 次,明顯比另外兩條少得多。因為它不是隨手就能養成的習慣,你得先意識到「這個值可能是 0」。越需要思考的規則,落地率越低。)
一、fix 比例高,會影響外部對團隊的評價。 尤其當公司用「bug 數量」當品質指標的時候。我沒有好的解法,只能提早溝通、並且做好分類。
二、三類分開統計需要紀律。 修 bug 的當下最想做的事是趕快修完,不是分類。而事後補分類幾乎不可能。我們做得不完整,這是實話。
三、防禦性寫法會讓程式碼變囉嗦,而且可能掩蓋問題。 res?.data?.list ?? [] 的意思是「拿不到就當空的」,但有時候「拿不到」本身就是個該被發現的錯誤。全部兜底的結果是畫面永遠正常,而問題被靜靜吞掉。
第三點是我目前還沒有好答案的部分。兜底和吞錯誤之間的界線很細,而我們可能偏向了兜底那邊。
?.」有用。>= 0 判斷三十一次——差別不在重要性,在「要不要先想一下」。明天 Day 24 講重構裡最痛快的一件事:有一次改動,刪掉的行數是新增的十四倍。而我會論證:衡量重構成效,要看行數變化的方向。