iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Modern Web

一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年系列 第 23

Day 23|將近一半的改動是在修 bug,而我認為這是好消息

  • 分享至 

  • xImage
  •  

模組五|重構期的品質與節奏(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 分類

「修 bug」這個標籤底下,其實混了三種完全不同的東西。

第一類:搬遷失真

舊系統有一個行為,新系統搬過去的時候漏了。

這類最多,而且幾乎都不是「寫錯」,是「不知道有這件事」。

Day 16 舉過的例子:舊系統的查詢,日期留空預設查最近七天。這個行為沒有寫在任何規格裡,只存在於六年前那個人寫的一行程式碼裡。

重寫的人不會知道。他會照著「日期留空 = 不限制」的直覺寫,然後上線後被回報。

這類 bug 是遷移的必然成本,不是能力問題。

第二類:對新環境的假設錯誤

新框架、新元件庫、新的響應式機制(資料一變、畫面自動跟著更新的那套機制),都有自己的脾氣。

Day 8 講過的 reactive(Vue 3 建立響應式資料的兩種寫法之一)解構會失去響應性——欄位一旦被拆成獨立變數,就跟原本的資料斷了關係——就是這類,你以為它會動,它不會,而且不報錯。

這類 bug 有個特徵:同一種錯誤會在短時間內集中出現,然後隨著大家熟悉而消失。 它是學習曲線的具體形狀。

第三類:舊系統本來就有的 bug

這一類最有價值,也最容易被誤會。

重構的時候,你被迫逐行讀過每一個檔案。而讀的過程中,你會看到一些「這樣寫應該會出事」的地方,然後去試,然後發現它真的會出事。

一個我們實際遇到的類型:某個數值計算在特定情況下型別會跑掉,導致金額顯示錯誤。這個問題在舊系統存在很久,但因為觸發條件很窄,沒有人回報過。

而它被修掉,不是因為有人回報,是因為有人在搬的時候讀到了那段程式碼。

所以:重構是最好的 bug 掃描器

我把這塊壓了很久的石板扳起一個角,底下的東西全都露出來了

第三類 bug 解釋了為什麼 fix 比例高不是壞事。

你搬家的時候,會把每一個櫃子、每一個抽屜都打開一次。

然後你會發現:床底下有一隻壞掉的插座、櫃子後面的牆壁有一片壁癌、廚房那個抽屜的軌道其實早就歪了。

這些問題不是搬家造成的。 它們一直都在,只是你平常不會把床搬開。

重構就是那個把床搬開的動作。

而這也帶出一個反直覺的判準:

重構期的修復數量,不是品質指標,是「你看得多仔細」的指標。

真正該擔心的是修復數量太少——那通常代表大家只是機械地把程式碼從左邊複製到右邊,沒有真的讀懂它。

但這件事很難對外解釋

道理講得通,不代表溝通會順利。

實際上你會遇到的場景是:

業務:「新系統上線之後,問題怎麼比以前多?」

而這個問題你不能用「重構是最好的 bug 掃描器」來回答,因為那聽起來像藉口。

我們後來學到的做法是把三類 bug 分開統計,然後這樣講:

  • 這些是搬遷漏掉的 → 我們的責任,正在修
  • 這些是新環境的坑 → 一次性的學習成本,會隨時間下降
  • 這些是舊系統本來就有的 → 這些問題你們以前就在承受,只是現在被發現了

第三類特別重要,因為它會改變對話的性質:從「你們把東西弄壞了」變成「原來我們一直有這個問題」。

而要能這樣講,前提是你在修的時候就要標記它是哪一類。事後才想分類,會分不出來。

系統性的回應:防禦性寫法

我把最後一塊軟墊也鋪上去了,從此什麼掉下來都不會有聲音

分類之外,我們還做了一件事:針對最常出錯的模式,訂出寫法慣例。

第一類(搬遷失真)和第三類(舊有 bug)有一個共同的高發區:處理後端回傳的資料。

因為後端回什麼、有沒有可能是空的、巢狀結構長怎樣,這些在舊系統裡通常沒有型別、也沒有文件。

所以團隊訂了幾條很具體的慣例:

一、外部資料一律假設可能是空的

// 不要
dataSource.value = res.data.list

// 要
dataSource.value = res?.data?.list ?? []

這兩個符號在做的事:?. 叫可選鏈,中間任何一層是 nullundefined 就整條直接回傳 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 ?? [] 的意思是「拿不到就當空的」,但有時候「拿不到」本身就是個該被發現的錯誤。全部兜底的結果是畫面永遠正常,而問題被靜靜吞掉。

第三點是我目前還沒有好答案的部分。兜底和吞錯誤之間的界線很細,而我們可能偏向了兜底那邊。

帶走什麼

  1. 重構期的修復數量不是品質指標,是「你看得多仔細」的指標。 數量太少才該擔心。
  2. 把 bug 分成三類:搬遷失真、新環境的坑、舊系統本來就有的。 第三類會改變對話的性質,但你必須在修的當下就標記。
  3. 針對高發區訂具體的寫法慣例,不要訂原則。 「要小心空值」沒有用,「外部資料一律加 ?.」有用。
  4. 越需要思考的規則,落地率越低。 可選鏈六千次,>= 0 判斷三十一次——差別不在重要性,在「要不要先想一下」。
  5. 注意兜底和吞錯誤的界線。 全部加上預設值,會讓畫面永遠正常,也會讓問題永遠不被發現。

明天 Day 24 講重構裡最痛快的一件事:有一次改動,刪掉的行數是新增的十四倍。而我會論證:衡量重構成效,要看行數變化的方向。


上一篇
Day 22|語系從 4 個變成 11 個,維護成本反而下降了
下一篇
Day 24|最好的一次重構,刪掉的是新增的十四倍
系列文
一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言