第 8 / 8 部 待錄

重構API與處理繼承

《Refactoring 重構》Podcast 準備稿:重構 API・處理繼承

書名: 重構 Refactoring(Improving the Design of Existing Code, 2nd Edition) 作者: Martin Fowler 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Refactoring.好介面與繼承的取捨(完結) (8/8) 涵蓋範圍: 第11章 Refactoring APIs(重構 API)+ 第12章 Dealing with Inheritance(處理繼承)+ 全季收束

背景速覽

啃完前面七集的觀念與手法,最後一集處理兩個「比較進階、也比較有設計味」的主題,並替整季收尾。第11章 Refactoring APIs 談「怎麼設計好的模組介面」——查詢與修改分離、移除旗標引數、查詢與參數的取捨、函式與命令物件的互換。第12章 Dealing with Inheritance 談繼承體系的整理(上拉/下推成員、用子類別取代型別碼、繼承與委託的互換)。兩章合起來,是從「函式級」走向「模組與型別架構級」的重構,難度與威力都升一階。

一句話重點

好的 API 讓呼叫端「容易做對、難以做錯」——查詢和修改要分開、旗標引數要拆成明確函式;而繼承是把雙面刃,Fowler 的建議是**「先用繼承,需要時再換成委託」**,因為繼承好用但僵硬,委託靈活但囉嗦,沒有哪個永遠對。

值得討論的重點

1. Separate Query from Modifier:問問題不該有副作用

一個函式如果「回傳一個值,同時又偷偷改了狀態」,呼叫端就很難安全使用它——你不能隨意多呼叫一次來看看結果(會產生副作用)。Separate Query from Modifier 把「查詢(只回傳、無副作用)」和「修改(改狀態)」拆成兩個函式。這是 命令查詢分離原則(CQS, Command-Query Separation) 的直接操作:純查詢可以隨意呼叫、快取、測試,這份可預測性非常值錢。

2. Remove Flag Argument:旗標引數讓呼叫端讀不懂

book(customer, true) 裡那個 true 是什麼意思?呼叫端完全看不出來,得跳進函式定義才知道 true 代表「優先訂位」。Remove Flag Argument 把它拆成意圖明確的 bookPremium(customer) / bookRegular(customer)。注意 Fowler 的提醒:多個旗標引數別硬拆(會組合爆炸),而是先建幾個簡單函式、再用它們組合出原邏輯。核心:函式名就該說清楚你要做什麼,別讓呼叫端用一個 boolean 來猜。

3. 查詢 vs 參數、建構子 vs 工廠:一組「該由誰負責」的取捨

第11章有好幾對互逆操作,本質都在問「這個資訊該由函式自己去拿,還是由呼叫端傳進來?」:

  • Replace Parameter with Query / Replace Query with Parameter:能由函式自己算出來的就別當參數傳(減少參數列);但若這會讓函式偷偷依賴難測的全域狀態,就反過來把它變參數傳進來(提高可測性、純度)。這對的取捨核心是:你更在乎「呼叫簡潔」還是「函式純粹好測」。
  • Replace Constructor with Factory Function:建構子有諸多限制(名字固定、不能回傳子型別);換成工廠函式就能有意圖清楚的名字、能依參數回傳不同子類別。

4. Replace Function with Command:把函式升級成物件

當一個函式需要 undo/redo、需要排隊延遲執行、需要複雜的中間狀態時,把它包成一個 Command 物件(命令模式)會比一個純函式靈活得多。但這是有成本的取捨——命令物件帶來更多結構和複雜度,所以也有反向的 Replace Command with Function:殺雞別用牛刀,簡單的事用函式就好。

5. 繼承的整理:上拉、下推、用子類別取代型別碼

第12章前半是繼承體系的「家務整理」:

  • Pull Up(上拉)/ Push Down(下推):把多個子類別都有的相同成員「上拉」到父類別(消除重複);把只有某些子類別用得到的成員「下推」回去(避免父類別承載不該有的東西)。
  • Replace Type Code with Subclasses:當你用一個 typeCode 欄位配 switch 來區分行為(接 EP7 的多型重構),把每個型別變成一個子類別,行為差異就用覆寫表達。

6. 繼承 vs 委託:本季最重要的設計取捨之一

第12章後半(Replace Subclass with Delegate / Replace Superclass with Delegate)處理繼承最大的危險:繼承只能順單一軸線演化,而且父子關係太緊,改父類別容易波及所有子類別(EP2 的 Refused Bequest 味道就是繼承用錯)。委託(組合)更靈活,能多軸組合、耦合更鬆。

Fowler 的務實建議很值得當金句:「先用繼承,之後需要時再換成委託,毫無困難。」 因為繼承簡單直接、多數情況夠用,而從繼承改成委託是條成熟的重構路;反過來預先全用委託,常常是過度設計。這正是「組合優於繼承」這句老格言的動態版——不是叫你永遠別用繼承,是叫你別讓繼承僵在那裡擋路。

注意事項

⚠️ 「組合優於繼承」不等於「永遠別用繼承」。 Fowler 明確說繼承多數情況都能妥善解決問題,他的策略是「先繼承、需要時再改委託」。提醒聽眾:那句老格言是在警告「別過度依賴繼承」,不是禁用繼承——一上來就為了避免繼承而堆一層層委託,是另一種過度設計。

⚠️ 第11、12章的手法牽動的是「介面」與「型別架構」,是高風險區。 改 API 會影響所有呼叫端(尤其跨越公開邊界、別人也在用時),改繼承體系會牽動整個家族。這兩章比前面更需要 EP3 的測試安全網,也更需要 EP2 的「保留舊介面、標 Deprecated、漸進遷移」策略。

⚠️ 互逆操作(查詢與參數、建構子與工廠、函式與命令、繼承與委託)成對出現,再次說明「沒有絕對正確的方向」。 每一對都是一個 trade-off 的兩端,選哪邊取決於當下的需求(要簡潔還是要可測?要靈活還是要簡單?)。整本書到這裡,這個訊息應該已經非常清楚——重構不是把 code 推向某個唯一正確的終點,而是依需求來回塑形。

專家補充

💡 Separate Query from Modifier 背後的 CQS(命令查詢分離)是 Bertrand Meyer 提出的老原則,它的現代放大版就是架構界的 CQRS(命令查詢職責分離)——把讀和寫拆成不同的模型甚至不同的服務。一個函式級的小重構,連得到一個系統級的架構模式,這條線很能展現「重構手法其實是設計原則的微縮」。

💡 「先繼承後委託」這個建議,體現了 Fowler 整本書最深的方法論信念——不要試圖一次設計到位,而要保留演化的彈性。 你不必在資訊最少的專案初期,就賭對「該用繼承還是委託」;先選簡單的(繼承),等你更懂問題、發現繼承擋路了,再用成熟的重構路換成委託。這就是「演進式設計」的精髓:好設計是長出來的,不是一開始畫出來的。 這句話適合當整季的收束主旨。

💡 整本《重構》的 60 幾個手法,超過一半是「成對的互逆操作」。這個結構本身就是 Fowler 最大的教學:重構的智慧不在於「知道哪招對」,而在於「知道現在該往哪個方向塑形」。 同一段 code,今天該 Extract,明天可能該 Inline;今天該繼承,明天該委託。能讀出「現在缺什麼、該往哪走」的工程直覺,才是這本目錄真正想培養的東西——而那只能靠不斷練習養成,背招式沒用。

討論問題

🎙️ 開場鉤子:「book(customer, true)——這個 true 是什麼意思?你猜不到吧?今天這集就從『怎麼設計一個不用猜的介面』講起,然後聊一個吵了二十年的問題:繼承到底好不好用。」

🎙️ 自問自答:「繼承和組合,我到底該用哪個?」——Fowler 的答案很務實:先用繼承(簡單夠用),等你發現它擋路了(只能單軸演化、改父類波及全家),再用成熟的重構路換成委託。別在最不懂問題的時候就賭一個架構。

🎙️ 帶走的一題(整季收尾):「啃完這八集,別想著背 60 幾招。挑你 code 裡最讓你頭痛的一段,問自己一句:它現在缺什麼、該往哪個方向塑形?然後走一小步、跑一次測試。重構的功夫,是這樣一步步練出來的。」

更大範圍關聯

  • 軟體工匠三經典的收束:《重構》(怎麼安全地把髒改乾淨)+《Clean Code》(乾淨長什麼樣)+《The Pragmatic Programmer》(務實的工程態度),三本互為表裡。啃完《重構》,等於把「持續改善設計」這項核心手藝補齊了(見 [軟體工程領域切分藍圖])。
  • 與《Working Effectively with Legacy Code》的分工:本書假設你有測試、能安全地小步前進;當你面對「糾纏到無法測、無法安全動」的祖傳 code,Michael Feathers 教你怎麼先撬出接縫、架起安全網——Feathers 負責把老 code 救到能被重構的狀態,Fowler 接手把它變好。兩本是維護工程的左右手。
  • 與設計原則/模式的關係:這兩章密集對應 CQS/CQRS、命令模式、工廠模式、組合優於繼承、開閉原則——重構手法是這些原則和模式的「操作指令集」。讀懂了原則,手法就不是死記。
  • 演進式設計的方法論:整本書最終指向一個立場——好設計是透過重構「長」出來的,不是大設計先行「畫」出來的。這是敏捷時代軟體設計觀的基石,也是這趟啃書之旅的終點。

錄製建議

  • 建議時長:約 22-24 分鐘(合併兩章 + 全季收尾,可略長)。配比:重構 API(CQS、旗標引數、查詢與參數的取捨)約 8 分、處理繼承(上拉下推、型別碼→子類別)約 5 分、繼承 vs 委託(重頭戲)約 6 分、全季收束約 4 分。
  • 討論策略:API 那段用 book(customer, true) 的「猜 boolean」痛點開場,收束到「好介面讓人容易做對、難以做錯」。繼承那段把「先繼承後委託」當金句反覆敲,並誠實呈現「組合優於繼承」這句老格言的分寸(是警告不是禁令)。最後一定要做全季收尾:回扣「重構=小步+測試保護網+兩頂帽子」的心法,並把「重構的功夫不在背招、在養出『該往哪塑形』的直覺」當作整季的臨別贈言。
  • 這是完結集,記得花 1-2 分鐘回顧這趟從「第一個範例」到「繼承取捨」走過的路,給聽眾完成一本大書的成就感,並預告/銜接系列的下一本。