第 6 / 8 部 待錄

組織資料

《Refactoring 重構》Podcast 準備稿:組織資料

書名: 重構 Refactoring(Improving the Design of Existing Code, 2nd Edition) 作者: Martin Fowler 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Refactoring.組織資料:一個變數只扛一個責任 (6/8) 涵蓋範圍: 第9章 Organizing Data(組織資料)

背景速覽

前兩集處理「行為(函式)」的封裝與搬移,這集回到更底層的「資料」本身。第9章 Organizing Data 篇幅較短但很精,談的是怎麼把資料的角色理清楚:Split Variable(拆分變數)、Rename Field、Replace Derived Variable with Query(用查詢取代衍生變數)、以及一對互逆的 Change Reference to Value / Change Value to Reference。核心關懷是兩件事:一個變數只該有一個責任,以及資料該用「值」還是「參考」來建模

一句話重點

資料混亂是 bug 的溫床——一個被反覆重新賦值、身兼數職的變數會讓讀者徹底困惑;把它拆成「一個變數一個責任」,再想清楚這份資料到底是「不可變的值」還是「需要共享的參考」,程式的可預測性就上來了。

值得討論的重點

1. Split Variable:一個變數,只扛一個責任

最常見的壞習慣之一:一個變數 temp 先拿來累加總和、算完平均後又被重新賦值拿去存一句訊息。它「多次被重新賦值」這件事本身就是訊號——它的責任已經不只一個了。Split Variable 就是給每個責任一個獨立的、命名清楚的變數(totalaveragemessage)。例外是「迴圈變數」和「累加變數」這種本來就該反覆賦值的——那是它們的正常工作,不算多重責任。判準:它的每一次賦值,是不是代表「同一件事的更新」? 不是的話,就該拆。

2. Replace Derived Variable with Query:能算出來的,就別存

如果一個變數的值完全可以從別的資料推導/計算出來(衍生資料),那把它存成一個欄位就是在製造「兩份真相」——一旦來源變了而你忘了同步,它就是錯的。改成一個 query(每次要用就現算),就消滅了「忘記同步」這整類 bug。這招直接呼應 EP2 的 Mutable Data 味道:可變狀態越少,要擔心的同步問題就越少。

3. Reference vs Value:這份資料該怎麼建模?

第9章最有設計味的一對手法,是 Change Reference to Value / Change Value to Reference(互逆)。問題是:當物件 A 內含物件 B,B 該是「參考」還是「值」?

  • 用值(Value):B 不可變,每個持有者各自擁有一份。值物件因為不可變所以好理解、好處理——你不必擔心別人偷偷改了它害你出錯,在分散式、並行系統裡尤其安全(沒有共享可變狀態)。適合:日期、金額、座標這類「只看內容、不看身分」的東西。
  • 用參考(Reference):多個物件需要共享同一個 B,而且 B 改了大家都要看到最新的。適合:訂單與客戶的關係——客戶改了地址,所有訂單都該反映。

判準鏈:多個物件要共享嗎?要的話,B 會被改、且改動要傳播給大家嗎? 要 → Reference;不要(或根本不可變)→ Value。

4. Rename Field:資料的名字也是文件

欄位改名看似瑣碎,但資料結構的欄位名往往是整個系統的詞彙表——一個 dt 改成 transactionDate,影響的是每一個讀到它的人對領域的理解。在有封裝(EP4)的前提下,改欄位名可以漸進進行(先包成 getter,再逐步遷移),這也是「先封裝才好改」的又一個例證。

注意事項

⚠️ 「值物件好、參考不好」是個過度簡化。 Change Reference to Value / Change Value to Reference 是一對互逆操作,正說明兩者各有適用場景。值物件雖然好理解,但當你需要「一處修改、處處可見」的共享語意時,硬用值就會製造一堆不同步的副本。判準永遠是「需不需要共享 + 需不需要傳播變更」,不是「哪個比較高級」。

⚠️ Split Variable 別把正常的累加/迴圈變數也拆了。 迴圈計數器 i、累加器 sum 在迴圈裡反覆賦值是它們的本職,不是多重責任。判準是「每次賦值是不是同一件事的更新」,不是「有沒有被賦值超過一次」。

⚠️ Replace Derived Variable with Query 要留意效能極端情況。 絕大多數時候「現算」的成本可忽略,且省下的同步 bug 遠值回票價。但若某個衍生值計算極貴又被高頻讀取,那是少數需要快取的例外——而那也該是量測後的決定,不是預設行為。

專家補充

💡 「能算出來的就別存」(Replace Derived Variable with Query)背後是一個很深的原則——單一真相來源(Single Source of Truth)。系統裡每一個事實,最好只有一個地方說了算;任何「冗餘存一份」的資料,都是一個等著和來源不同步的定時炸彈。這條原則從資料庫正規化、到前端狀態管理(Redux 的 derived state / selector)、到這裡的重構,是同一個智慧的不同化身。

💡 Value Object 是 Eric Evans《Domain-Driven Design》的核心建模元件之一(與 Entity 相對)。DDD 的區分標準正是這集講的:Entity 有身分、可變、要追蹤(用參考);Value Object 無身分、不可變、可替換(用值)。Fowler 本人深度參與了 DDD 社群,這集的 Reference/Value 手法可以直接接到 DDD 的戰術建模,給聽眾一條往上走的路。

💡 「不可變資料更好理解」這個主張,是函數式程式設計的核心信條之一,近年大舉影響主流:Rust 的 ownership、React 的不可變 state、Kotlin 的 val/data class,全在推「預設不可變」。Fowler 在 Mutable Data 味道和這集都明確站隊「能不可變就不可變」——這不是潔癖,是把一整類並行/同步 bug 從源頭消除。

討論問題

🎙️ 開場鉤子:「你有沒有看過一個叫 temp 的變數,在一個函式裡被賦值五次,每次裝的還是不同種東西?讀那種 code 像在解謎。今天第一招就是專治這個。」

🎙️ 自問自答:「一份資料,我到底該用值還是參考?」——先問:別人需要共享它嗎?需要的話,它改了大家都得看到嗎?兩個都『是』就用參考;只要它本質不可變、或不需共享,就用值(更好理解、並行更安全)。

🎙️ 帶走的一題:「找一個你存在欄位裡、但其實可以從別的資料算出來的『衍生值』,把它改成一個 query。你會少掉一整類『忘記更新它』的 bug。」

更大範圍關聯

  • 與 DDD(Domain-Driven Design):Reference/Value 之分對應 Evans 的 Entity/Value Object 建模;這集是 DDD 戰術設計的微觀入口,Fowler 與 DDD 社群淵源很深。
  • 與函數式程式設計:「不可變優先」「衍生值用算的別用存的」是 FP 滲透主流的兩條主線,從 Redux 到 Rust 都看得到。
  • 單一真相來源:從資料庫正規化到前端狀態管理,「事實只有一個地方說了算」是貫穿整個資訊領域的智慧,這集是它在程式碼層級的展現。
  • 與封裝(EP4)的接力:先封裝(給資料一個入口),才好做改名、拆分、改建模——這集很多招都建立在 EP4 的封裝之上(見 [軟體工程領域切分藍圖])。

錄製建議

  • 建議時長:約 17-18 分鐘(本章篇幅較短,是這幾集裡可以稍微輕鬆的一集)。配比:Split Variable 約 5 分、Replace Derived Variable with Query(接單一真相來源)約 5 分、Reference vs Value(接 DDD)約 6 分、Rename Field 約 2 分。
  • 討論策略:用「一個 temp 裝五種東西」的畫面開場,把「一個變數一個責任」講成可讀性的鐵律。中段把「能算就別存」拉高到「單一真相來源」這個跨領域智慧,顯縱深。Reference/Value 那段是這集的設計重頭戲,務必把判準鏈(要共享嗎→要傳播變更嗎)講成可操作的決策樹,並接到 DDD 給聽眾往上走的路。
  • 這集適合多談「為什麼不可變更好」——這是近十年最重要的設計思潮之一,講透很有價值。