第 4 / 8 部 待錄

封裝

《Refactoring 重構》Podcast 準備稿:封裝

書名: 重構 Refactoring(Improving the Design of Existing Code, 2nd Edition) 作者: Martin Fowler 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Refactoring.封裝:把資料藏好、把模組劃清 (4/8) 涵蓋範圍: 第7章 Encapsulation(封裝)

背景速覽

從這集開始,連續幾集走進手法目錄的「分類」階段——每集鎖定一類重構,挑出最重要的幾招示範,重點放在背後的設計原則而非逐招硬背。這集主題是「封裝」:第7章談的是「怎麼控制什麼東西能被看見、被存取」。封裝是物件導向最核心的承諾,這章的手法(Encapsulate Record / Collection、Replace Primitive with Object、Extract / Inline Class、Hide Delegate / Remove Middle Man)都在回答同一個問題:一個模組該對外露出多少、藏起多少?

一句話重點

封裝的本質是控制「變更的傳播範圍」——把資料和對它的操作圈在一起、藏在介面後面,這樣將來要改它的內部時,外界不會跟著一起崩;封裝做得好不好,直接決定你改一個東西要動幾個地方。

值得討論的重點

1. Encapsulate Variable / Record / Collection:先包起來,才好改

封裝章的第一組手法都在做同一件事:把「裸露的資料」包進存取函式或物件後面

  • Encapsulate Variable:當一個資料被很多地方直接讀寫(尤其是全域資料),你幾乎不可能安全地改它。先用 getter/setter 把它包起來,所有存取走同一個入口,之後要改名、改型別、加驗證都有了單一施力點。
  • Encapsulate Record:把到處傳遞的 hash / map / dict(隱晦的資料結構)換成有明確欄位的類別。小範圍用 dict 沒問題,但用的範圍一大,「結構隱晦」這個缺點就值得用 Data Class 換掉——你會得到明確的欄位名、可以漸進改名、可以掛行為。
  • Encapsulate Collection:別直接把內部的 list 回傳出去(外界拿到就能繞過你亂改),回傳唯讀視圖或副本,並提供 add/remove 方法控制變更入口。

核心心法:封裝給你一個「單一施力點」,所有後續的改動都從這個點發力,而不是散彈式地改一百個呼叫端。

2. Replace Primitive with Object:別用 string 硬撐業務概念

這招直接對應 EP2 的「Primitive Obsession(基本型別偏執)」味道。當你用 string 存電話號碼、用 number 存金額,一開始很方便,但很快你會發現格式化、驗證、比較的邏輯散落各處。把它升級成一個小物件(PhoneNumber、Money),這些行為就有了歸宿。這是「資料想要長出行為」的訊號——一旦你發現某個基本型別老是被一群函式圍著轉,它其實想變成一個類別。

3. Extract Class / Inline Class:類別職責的拆與合

  • Extract Class:類別會隨時間「責任變重」,膨脹到失去彈性。拆的時機:看到「某群資料和方法總是一起變動、互相依賴」,就把它們抽成新類別。Fowler 給一個漂亮的自問——「如果我移除某段資料,會不會讓其他欄位或方法變得不合理?」 會的話,它們就是一夥的,該一起抽出去(範例:把 street/city/zip 從 Employee 抽成 Address)。
  • Inline Class:反過來,當一個類別已經沒什麼職責、變成空殼,就把它塞回它最該待的地方。又是一對互逆操作。

4. Hide Delegate / Remove Middle Man:迪米特法則的進退

  • Hide Delegate:當客戶端寫出 person.getDepartment().getManager() 這種長鏈(對應 Message Chains 味道),你把中間的委託藏起來,提供 person.getManager(),客戶端就不必知道「經理是透過部門拿到的」這個內部結構。
  • Remove Middle Man:但如果一個類別過半的方法都只是「轉手」給另一個類別(Middle Man 味道),那封裝就過頭了,反而該移除中間人、讓客戶端直接溝通。

這對互逆操作其實是在問:迪米特法則(Law of Demeter)該守到什麼程度? 答案是動態的——隨著委託關係的多寡來回調整,沒有一勞永逸的標準。

注意事項

⚠️ 封裝不是「全部加 getter/setter」。 無腦給每個欄位配一對 getter/setter,其實只是把欄位換個方式公開出去,封裝的精神(隱藏內部決策、控制變更)一點也沒達到——這正是 EP2 提過的 Data Class 味道。封裝的重點是藏住內部結構與行為決策,不是機械式地包一層。

⚠️ Hide Delegate 和 Remove Middle Man 沒有「正確答案」。 它們是一對互逆操作,意味著封裝程度是個動態平衡:委託多了就藏(Hide Delegate),藏到變傳話筒了就移除(Remove Middle Man)。別讓聽眾以為「永遠要隱藏委託」——過度封裝會讓一個類別變成只會轉手的中間人。

⚠️ Replace Primitive with Object 很好用,但別走火入魔到「為每個 string 都造一個類別」。判準還是 EP2 那條:這個基本型別周圍是不是已經圍著一群處理它的邏輯? 有,才升級;沒有,留著就好。

專家補充

💡 封裝整章其實在講同一個更大的概念——資訊隱藏(Information Hiding),這是 David Parnas 1972 年那篇經典論文〈On the Criteria To Be Used in Decomposing Systems into Modules〉的核心。Parnas 的洞見是:模組的邊界該畫在「最可能變動的設計決策」周圍,把它藏起來,讓變動不外溢。Fowler 這些封裝手法,全是這個 50 年前洞見的具體操作版。這條思想史的線拉出來很有份量。

💡 Extract Class 那個自問——「移除這段資料會不會讓其他東西變不合理」——其實是在測量內聚力(cohesion)。一群「彼此缺了就不合理」的資料和方法,就是高內聚的一坨,它們想成為一個類別。這個直覺比「一個類別不該超過 N 行」這種行數規則深刻得多,值得在節目裡反覆強調:好的拆分靠內聚直覺,不靠行數計數。

💡 「Replace Primitive with Object → 進而 Replace Type Code with Subclasses」是一條漂亮的演化路線:先把基本型別升級成物件,等你發現這個物件的行為會「依型別而異」,再進一步用子類別/多型展開(EP8 會講到)。封裝是很多後續重構的起點——你得先把東西包成物件,才有地方掛多型。可以預告這條線。

討論問題

🎙️ 開場鉤子:「你有沒有改過一個欄位的名字,結果 grep 出來一百個地方在直接讀它,你改到懷疑人生?今天這集就是在講,怎麼讓『改一個東西』不再是場災難。」

🎙️ 自問自答:「封裝不就是把欄位變 private、加 getter/setter 嗎?」——不是。那只是換個方式公開。真正的封裝是藏住內部結構和決策,給你一個未來改動的單一施力點;無腦 getter/setter 反而是 Data Class 臭味。

🎙️ 帶走的一題:「找一個你 code 裡到處被傳來傳去的 dict 或 map,把它 Encapsulate 成一個有明確欄位的類別。你會發現,光是這一步,很多隱藏的耦合就現形了。」

更大範圍關聯

  • 思想源頭:封裝章是 Parnas「資訊隱藏」與物件導向「封裝」原則的操作手冊。理解了「藏住會變的東西」這個目的,這些手法就不再是孤立招式。
  • 與 SOLID 的依賴反轉/開閉原則:封裝讓「對擴充開放、對修改封閉」成為可能——把內部藏好、只露出穩定介面,你才能在不動既有呼叫端的情況下替換實作。
  • 與《Clean Code》的物件與資料結構:Uncle Bob 區分「物件(藏資料、露行為)」與「資料結構(露資料、無行為)」,Fowler 的封裝手法正是把後者升級成前者的工具,兩本在這裡對話。
  • 接下來的線:封裝(這集)→ 搬移特性(EP5)→ 處理繼承(EP8)是一條「先把東西包好、再決定放哪、最後處理型別差異」的演化路徑(見 [軟體工程領域切分藍圖])。

錄製建議

  • 建議時長:約 20 分鐘。配比:封裝資料三招(Variable/Record/Collection)約 7 分、Replace Primitive with Object 約 4 分、Extract/Inline Class 約 5 分、Hide/Remove Middle Man 約 4 分。
  • 討論策略:用「改一個欄位要動一百個地方」的痛開場,把整章收束到一句話——封裝=給未來的改動製造單一施力點。別逐招念,每招都回扣這個主軸。Extract Class 那個「移除會不會變不合理」的自問很好用,當成內聚力的判準反覆講。結尾預告「封裝是後面搬移、繼承重構的起點」,建立集數連貫感。
  • 強調互逆操作(Extract/Inline Class、Hide/Remove Middle Man)傳達的「封裝是動態平衡、不是越多越好」,避免聽眾矯枉過正。