🎯 什麼情境該想到我

當你「要改一個方法,卻卡在它的參數型別上——那個參數難以建立、難以偽裝,或根本是你改不動的標準介面」的時候。

⚙️ 怎麼用(步驟)

先判斷該不該用(p.258–259)

  • 在對方法作改動時常常會遇到一些令人頭疼的依賴,這些依賴由方法的參數導致:有時會發現難以建立所需的參數,有時是需要測試某方法對其某個參數的影響。書中說「許多時候我發現參數的型別正是帶來麻煩的根源」。
  • 如果該類別是我可以修改的,則優先用 介面提取(285頁):「在參數依賴問題上,介面提取(285頁)往往是不二之選。」
  • 但若「參數型別的抽象層次較低,或特定於某些實現技術的話,提取其介面就有可能達不到預期效果甚至根本就不可行」。
  • 因此:「當無法對一個參數的型別使用介面提取,或者當該參數難以『偽裝』的時候,可採用參數適配手法。」

核心做法:把參數型別外覆起來,改讓方法依賴一個自己定義的窄介面,「從而完全解除對 API 介面的依賴」(p.259)。書中的判準是:「介面應傳達職責而非實現細節。這樣的介面令程式碼易於閱讀和維護。」(p.260)

步驟(p.261)

  1. 建立將被用於該方法的新介面,該介面越簡單且能表達意圖越好。但也要注意,該介面不應導致需要對該方法的程式碼作大規模修改。
  2. 為新介面建立一個用於產品程式碼的實作。
  3. 為新介面建立一個用於測試的「偽造」實作。
  4. 編寫一個簡單的測試案例,將偽物件傳給該方法。
  5. 對該方法作必要的修改以使其能使用新的參數。
  6. 執行測試來確保你能使用偽物件來測試該方法。

★★ 在 Go 裡:這一招是免費的,而且是處理「重型參數」的首選

★ 書中的做法:★★ 為那個難用的參數型別「包一個介面」,★ 讓方法收介面
  → ★ 在 Java 裡這需要:★★ 建介面、★ 讓原型別實作它(★ 或寫一個 wrapper)
// ★★ Go 裡:★ 只需要宣告介面,★★ 原型別自動滿足(隱式滿足)
// ★ 問題:★★ 參數是 *http.Request,★ 但這個函式只需要讀兩個 header
func Authorize(r *http.Request) error { … }
 
// ✅ ★ 參數適配:宣告一個只有你需要的方法的介面
type headerReader interface{ Get(key string) string }
func Authorize(h headerReader) error { … }
// ★ 呼叫端:Authorize(r.Header)   ← ★★ http.Header 自動滿足(它有 Get 方法)
// ★ 測試:  Authorize(http.Header{"Authorization": {"Bearer x"}})

★★ 這是「參數適配」在 Go 最漂亮的形態:
★ 零 wrapper、零修改、而且編譯器驗證所有呼叫端。
★ 見 介面提取:★★ 同樣的免費性讓這本書一半的手法在 Go 不需要。

★★ 一個標準庫層級的實例:io.Reader

// ❌★★ 收一個具體的重型別
func ParseConfig(f *os.File) (Config, error)     // ★ 測試必須建一個真的檔案
 
// ✅ ★ 收最小的介面
func ParseConfig(r io.Reader) (Config, error)     // ★★ 測試:strings.NewReader("…")

★★ io.Reader 是「參數適配」的極致:★ 一個方法的介面,
★★ 而整個 Go 生態的所有位元組來源都滿足它(檔案、網路、記憶體、gzip、加密…)。
→ ★ 判準:★★ 「這個函式真正需要參數的什麼能力?」 ——★ 只收那個能力。

★★ Go 特有的三個實務細節

// ① ★★ 介面要定義在「使用它的那一側」,而且未匯出
package auth
type headerReader interface{ Get(key string) string }     // ✅ ★ 小寫
// → ★ 匯出它 = ★★ 宣告一個公開契約(★ 而你只是想收一個參數)
 
// ② ★★ 不要為了「更通用」而收太抽象的東西
func Process(v any) error                    // ★★ 這不是適配,是放棄型別
func Process(r io.Reader) error              // ✅ ★ 剛好的抽象
 
// ③ ★★ 注意「非 nil 的 nil 介面」
var f *os.File            // ★ nil
ParseConfig(f)            // ★★ 傳進去的介面「不等於 nil」⇒ ★ 方法呼叫會 panic
// → ★ 見 [[工具-Go介面小而隱式]]:★★ 介面值是 (型別, 值),型別非空就不是 nil

★★ ③ 是這一招唯一的 Go 特有陷阱:★ 把一個 nil 的具體指標傳進介面參數
→ ★★ if r == nil 的檢查會失敗(它不是 nil),而呼叫它的方法會 panic。

★★ 什麼時候用「參數適配」vs「樸素化參數」

參數適配樸素化參數
做什麼★ 把參數換成一個更小的介面★ 把參數拆成基本型別
★ Go 的成本★★ 零(隱式滿足)★ 要改簽章與呼叫端
保留語義★★ 保留(★ 仍是一個有意義的型別)★★ 失去(★ 變成一堆基本型別)
適合★ 參數有「你只用到一小部分」的方法★ 參數是一個純資料容器
★ Feathers 的評價★ 較好★★ 「不理想」

★★ 在 Go 裡:★ 參數適配幾乎總是優於樸素化參數,
★ 因為它零成本、保留語義、而且編譯器驗證。

★ 唯一的例外:★★ 那個參數型別沒有方法(★ 純資料 struct)
→ ★ 那時沒有介面可以抽,★★ 只能走 引入參數物件 或直接傳值。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 它違反簽名保持:「參數適配是一個違反簽名保持(249頁)的例子。因此,用的時候請格外小心。」(p.260)

  • 簡化過頭會出 bug:「有些時候參數適配手法也可能會帶來危險,比如你建立的簡化介面與原來參數的介面型別相差太遠。如果我們在修改的時候不小心,就可能會引入微小的 bug。」(p.261)

  • 不要追求最好的結構,要追求最有把握的改動:「記住我們是為了將測試安置到位而解開依賴。所以應該去做那些你更有信心的修改,而不是能導致最佳程式碼結構的修改。一旦測試到位,一切都好辦了,那時你自然會得到最佳程式碼結構。」(p.261)書中舉例:之後或許才想把介面改成不用檢查回傳 null(空對象模式,94頁)。

  • 安全第一:「一旦測試到位,你便可以更有信心地進行侵入性的改動了。」(p.261)

  • 整章共通的誠實取捨:「這些技術並不能立竿見影地讓設計變得更好。實際上,如果你有良好的設計感,這裡的某些技術甚至可能會令你退縮。」(p.258)

  • 與《重構》的差別:書中特別註明,本章的一些手法在 Martin Fowler 的《重構》中也有描述,「只是本章對它們的描述在步驟上有所不同,我將之適當剪裁以使得它們能夠被安全地用在沒有測試的情況下」(p.258)。所以名字接近的重構手法(如 引入參數物件、改變函式宣告)目的不同:那些是在測試保護下改善結構,這裡是為了先寫得出測試而打破依賴。

  • ★★ 在 Go 裡這一招是免費的(隱式滿足):宣告一個只有你需要的方法的介面,原型別自動滿足、零 wrapper、零修改。

  • ★★ 在 Go 裡它幾乎總是優於 樸素化參數:零成本、保留語義、編譯器驗證。

  • ★★ 判準:「這個函式真正需要參數的什麼能力?」 只收那個能力(io.Reader 是極致)。

  • ★★ 介面要定義在使用它的那一側且未匯出。 匯出它等於宣告一個公開契約,而你只是想收一個參數。

  • ★★ 注意「非 nil 的 nil 介面」:傳一個 nil 的具體指標進介面參數 → if r == nil 檢查會失敗(它不是 nil),呼叫方法會 panic。

  • ★ 不要為了通用而收 any,那不是適配是放棄型別。

  • ★ 唯一不適用的情況是「那個參數型別沒有方法」(純資料 struct),那時走 引入參數物件。

🔗 相關工具