模組六|工程文化與收斂(Day 27–30)
兩套系統要做同一個功能。你會怎麼做?
工程師的本能是:寫一份,兩邊共用。
我們沒有。我們刻意先寫了兩份,等了幾個月,確認它們真的一樣,才合併。
今天講這個判斷,以及我們在這件事上做對和做錯的地方。

大部分人學到的第一條原則是 DRY:不要重複自己。
Day 3 講過它常被誤讀:DRY 要消除的是「同一份知識存成兩份」,不是「兩段程式碼長得像」。
而太早合併的代價,比重複更貴:
你抽出一個共用元件,兩邊都用它。
三個月後,A 系統的需求變了。你在共用元件裡加第一個
if。
半年後,B 系統的需求也變了。第二個if。
一年後,第十個if。現在那個共用元件沒有人敢改,因為改它會同時影響兩個產品,而你已經看不出哪個分支是給誰用的。
重複的程式碼你可以放心改一邊。錯誤的抽象逼你每次都要考慮兩邊。
用一個場景:
三個同事都說「想喝飲料」。
你很有效率地訂了一箱可樂。
結果一個要無糖、一個要熱的、一個其實只是想喝水。
他們說的是同一句話,要的不是同一件事。
專案裡有一份需求文件,記錄了一個功能的完整過程。它的結構讓我印象很深,因為它把階段寫得非常明確:
Phase 1:初始實作
新增一個獨立目錄,把功能完整做出來
(彈窗、hook、假資料,全部自己一份)
Phase 2:重構遷移
刪除那個獨立目錄
遷移進共用目錄,重構成兩種場景共用
用一個 type 參數區分差異
兩個階段之間隔了幾個月。
因為第一次做的時候,你根本不知道兩邊的差異在哪裡。
你以為的差異,和真實的差異,通常不一樣。
具體來說,在第一次實作完成之前,這些問題你都答不出來:
而如果你在還答不出這些問題的時候就抽共用元件,你抽的是自己的猜測。
Phase 1 的目的,就是把猜測變成事實。
沒有在做這件事。
那幾個月裡,第一套系統的功能上線了、被使用了、被回報了問題、改了幾次。
而那些改動,就是在告訴你「哪些部分是會變的」。
等到第二套系統要做同樣功能的時候,你手上已經有一份「這個功能實際上長什麼樣」的證據,不是設計稿,是活過幾個月的程式碼。

這是我覺得整個流程裡最值得抄的一步。
那份文件裡有一張表,明確列出兩邊的差異(欄位名我改寫過):
| 項目 | 系統 A | 系統 B |
|---|---|---|
| API 欄位命名 | configKey / configValue |
paramKey / paramValue |
| 類型代碼 | 依角色而定 | 固定值 |
| 提示文案 | 不顯示專屬項目 | 顯示啟用項目 |
| 功能擺放位置 | 內嵌在表格某一列 | 獨立分頁 |
四項差異。而且每一項都能被一個參數收斂。
這張表就是「該不該合併」的判斷依據:
我覺得這比任何原則都實用。因為它把一個抽象的判斷(「這兩個東西夠像嗎」)變成了一個具體的動作:把差異寫下來,然後數一數。
寫不出這張表,就代表你還不夠了解它們,那就還不到合併的時候。
if 堆疊合併之後的做法是:共用的 hook 吃一個 type 參數,內部靠它決定行為。
這跟「滿地 if」的差別在哪?
if:每個差異各自判斷,判斷條件散在各處,新增一種場景要找出所有分支換句話說:你要能說出「這個共用元件的變化維度是什麼」。 如果答案是「呃,很多地方不太一樣」,那它就還不該被共用。
講完做對的,講搞砸的。這兩個例子都很有代表性。
改動紀錄裡有這麼一組:
某日 抽出一個標籤渲染元件,統一三個模組的顯示邏輯
隔天 把那個元件替換成另一個統一的元件
隔一天。
第一天有人發現三個模組在做同樣的事,抽出來共用。第二天有人發現,這個新抽出來的東西跟另一個既有元件其實可以合併。
這件事我覺得沒有什麼好檢討的,反而是好事。它說明的是:
共用元件不是「抽出來就結束」,它自己也會演化。
如果把第一次的抽象當成永久決定,那第二天那個更好的合併就不會發生。
能接受自己抽的東西被再次重構,是共用層能持續健康的前提。
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 的教訓正是:等到夠痛的時候,兩邊已經長歪了。
明天是最後一天。我會回到 Day 1 開場的那三個數字,看看兩年之後它們變成什麼,以及哪些我當時承諾要做、但到現在還沒做的事。