iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Modern Web

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

Day 29|我們刻意先寫了兩份,隔了幾個月才合併

  • 分享至 

  • xImage
  •  

模組六|工程文化與收斂(Day 27–30)

兩套系統要做同一個功能。你會怎麼做?

工程師的本能是:寫一份,兩邊共用。

我們沒有。我們刻意先寫了兩份,等了幾個月,確認它們真的一樣,才合併。

今天講這個判斷,以及我們在這件事上做對和做錯的地方。

為什麼直覺會害人

為了兩邊都勾得到,我又替這根鐵條折了一個彎,第十個

大部分人學到的第一條原則是 DRY:不要重複自己。

Day 3 講過它常被誤讀:DRY 要消除的是「同一份知識存成兩份」,不是「兩段程式碼長得像」。

而太早合併的代價,比重複更貴:

你抽出一個共用元件,兩邊都用它。

三個月後,A 系統的需求變了。你在共用元件裡加第一個 if
半年後,B 系統的需求也變了。第二個 if
一年後,第十個 if

現在那個共用元件沒有人敢改,因為改它會同時影響兩個產品,而你已經看不出哪個分支是給誰用的。

重複的程式碼你可以放心改一邊。錯誤的抽象逼你每次都要考慮兩邊。

用一個場景:

三個同事都說「想喝飲料」。

你很有效率地訂了一箱可樂。

結果一個要無糖、一個要熱的、一個其實只是想喝水。

他們說的是同一句話,要的不是同一件事。

我們實際走的兩階段

專案裡有一份需求文件,記錄了一個功能的完整過程。它的結構讓我印象很深,因為它把階段寫得非常明確:

Phase 1:初始實作
  新增一個獨立目錄,把功能完整做出來
  (彈窗、hook、假資料,全部自己一份)

Phase 2:重構遷移
  刪除那個獨立目錄
  遷移進共用目錄,重構成兩種場景共用
  用一個 type 參數區分差異

兩個階段之間隔了幾個月。

Phase 1 為什麼刻意不共用

因為第一次做的時候,你根本不知道兩邊的差異在哪裡。

你以為的差異,和真實的差異,通常不一樣。

具體來說,在第一次實作完成之前,這些問題你都答不出來:

  • 兩邊的資料欄位命名一樣嗎?
  • 兩邊的預設值一樣嗎?
  • 兩邊的權限判斷一樣嗎?
  • 這個功能在兩邊的擺放位置一樣嗎?

而如果你在還答不出這些問題的時候就抽共用元件,你抽的是自己的猜測。

Phase 1 的目的,就是把猜測變成事實。

中間那幾個月在做什麼

沒有在做這件事。

那幾個月裡,第一套系統的功能上線了、被使用了、被回報了問題、改了幾次。

而那些改動,就是在告訴你「哪些部分是會變的」。

等到第二套系統要做同樣功能的時候,你手上已經有一份「這個功能實際上長什麼樣」的證據,不是設計稿,是活過幾個月的程式碼。

Phase 2:合併之前先列差異表

兩邊的差異只有四項,我把它們一項一項串到同一根軸上

這是我覺得整個流程裡最值得抄的一步。

那份文件裡有一張表,明確列出兩邊的差異(欄位名我改寫過):

項目 系統 A 系統 B
API 欄位命名 configKey / configValue paramKey / paramValue
類型代碼 依角色而定 固定值
提示文案 不顯示專屬項目 顯示啟用項目
功能擺放位置 內嵌在表格某一列 獨立分頁

四項差異。而且每一項都能被一個參數收斂。

這張表就是「該不該合併」的判斷依據:

  • 列得完,而且差異之間不會互相影響 → 划算,合併
  • 列出來十五項,而且彼此糾纏 → 不要合,繼續各寫各的

我覺得這比任何原則都實用。因為它把一個抽象的判斷(「這兩個東西夠像嗎」)變成了一個具體的動作:把差異寫下來,然後數一數。

寫不出這張表,就代表你還不夠了解它們,那就還不到合併的時候。

差異用參數收斂,不是用 if 堆疊

合併之後的做法是:共用的 hook 吃一個 type 參數,內部靠它決定行為。

這跟「滿地 if」的差別在哪?

  • 滿地 if:每個差異各自判斷,判斷條件散在各處,新增一種場景要找出所有分支
  • 一個 type 參數:差異被收斂成一個明確的維度,新增場景就是多一個值

換句話說:你要能說出「這個共用元件的變化維度是什麼」。 如果答案是「呃,很多地方不太一樣」,那它就還不該被共用。

我們做錯的兩個地方

講完做對的,講搞砸的。這兩個例子都很有代表性。

一、抽出來,隔天又合併了一次

改動紀錄裡有這麼一組:

某日     抽出一個標籤渲染元件,統一三個模組的顯示邏輯
隔天     把那個元件替換成另一個統一的元件

隔一天。

第一天有人發現三個模組在做同樣的事,抽出來共用。第二天有人發現,這個新抽出來的東西跟另一個既有元件其實可以合併。

這件事我覺得沒有什麼好檢討的,反而是好事。它說明的是:

共用元件不是「抽出來就結束」,它自己也會演化。

如果把第一次的抽象當成永久決定,那第二天那個更好的合併就不會發生。

能接受自己抽的東西被再次重構,是共用層能持續健康的前提。

二、五個沒有被合併的 hook

Day 21 講過這個:專案裡有五個名字高度相似的 hook,都在做「檢查某個輸入是否有效」。

396 行、五個檔案、總共用在三個頁面。其中兩個完全沒人用。

這是兩階段流程的失敗案例:它們全部停在 Phase 1,而沒有人啟動 Phase 2。

為什麼?我的推測是:

一、它們是不同時間、不同人、為了不同需求寫的。 沒有人同時看到這五個。

二、每一個單獨看都不夠痛。 一個 87 行的 hook 用在一個頁面,不會有人覺得需要處理。

三、沒有人負責回頭看。 Day 21 講過那句話:寫一個 hook 是十分鐘的事,確認它有沒有被採用,是永遠沒有人做的事。

所以兩階段流程有個前提我當時沒寫出來:Phase 2 需要有人主動觸發。 它不會自己發生。

另一個層級:專案之間該共用什麼

前面講的是「功能」層級。最後講「專案」層級,順便把 Day 3 和 Day 6 的伏筆收回來。

Day 3 發現:兩個舊系統有 20 個檔案一字不差,另外 33 個差不到十行。

Day 6 發現:兩個新系統抽出了六個共用套件,但兩個 repo 各有一份,而且其中三個已經開始出現差異。

所以嚴格說,跨專案的重複問題,我們沒有解決。 只是把規模從 172 個檔案縮到 6 個套件。

那我們抽出來的那六個,有什麼共同特徵?

抽了 特徵
建置設定 跟業務無關
程式碼風格規則 跟業務無關
型別編譯設定 跟業務無關
通用 hooks(快取、防抖、節流、重試) 跟業務無關
共用型別 跟業務無關

全部跟業務無關。 而且它們的共用層本體很小:那包通用 hooks 只有兩百行左右。

沒有抽的:業務元件、頁面、API 定義。因為兩套系統的業務規則本來就在分岔。

而這個分界線,其實是 Day 3 那 20 個 diff = 0 的檔案早就標好的:它們全部都是基礎建設。

六年的實際使用行為,早就告訴你哪些東西不會分岔。你不需要猜。

判斷準則

把整篇收成四個問題,抽之前依序問:

一、它們現在真的一樣,還是只是長得像?
看語意,不是看行數。同一份知識 → 該合併;巧合的相似 → 不要。

二、如果 A 要改這個行為,B 會不會想要不一樣?
會 → 不要抽。這是最有效的一題。

三、兩邊的差異列得完嗎?
列不完 → 你還不夠了解它們,還不到時候。

四、差異能不能收斂成一個明確的維度?
不能 → 合併之後會變成滿地 if

代價

一、先寫兩份,代表有一段時間你真的有重複的程式碼。 如果那段期間需求改了,你要改兩次。這是這個策略的真實成本,不是零。

二、Phase 2 需要有人推。 那五個沒合併的 hook 就是證據。如果團隊沒有人負責回頭看,「先分開寫」會變成「永遠分開寫」,而那就只是純粹的重複,不是策略。

三、跨專案的共用,我們到現在也沒做。 兩個新系統仍是獨立的 repo,那六個套件各有一份、已經開始長歪。我們知道問題在那裡,但沒有處理,因為它還不夠痛。

而 Day 3 的教訓正是:等到夠痛的時候,兩邊已經長歪了。

帶走什麼

  1. 先容忍重複,等到你能把兩邊的差異列成一張表,才有資格抽象。 抽早了,你抽的是自己的猜測。
  2. 差異表就是判斷依據。 列得完而且能收斂成一個維度 → 合併;列不完或彼此糾纏 → 繼續分開。
  3. 共用元件不是抽出來就結束,它自己也會演化。 能接受自己抽的東西被再次重構,共用層才會健康。
  4. 兩階段流程的 Phase 2 需要有人主動觸發。 沒有人推,「先分開寫」就會變成「永遠分開寫」。
  5. 共用層要從「絕對不會分岔的東西」開始長,而不是從「現在看起來很像的東西」開始。

明天是最後一天。我會回到 Day 1 開場的那三個數字,看看兩年之後它們變成什麼,以及哪些我當時承諾要做、但到現在還沒做的事。


上一篇
Day 28|「這支 API 沒人用」——你敢刪嗎?
下一篇
Day 30|兩年之後,那個「一個測試檔」還是一個測試檔
系列文
一個活了六年的 Vue 2 後台,升級到 Vue 3 的那兩年30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言