🎯 什麼情境該想到我
當你「要改一個方法,卻卡在它的參數型別上——那個參數難以建立、難以偽裝,或根本是你改不動的標準介面」的時候。
⚙️ 怎麼用(步驟)
先判斷該不該用(p.258–259)
- 在對方法作改動時常常會遇到一些令人頭疼的依賴,這些依賴由方法的參數導致:有時會發現難以建立所需的參數,有時是需要測試某方法對其某個參數的影響。書中說「許多時候我發現參數的型別正是帶來麻煩的根源」。
- 如果該類別是我可以修改的,則優先用 介面提取(285頁):「在參數依賴問題上,介面提取(285頁)往往是不二之選。」
- 但若「參數型別的抽象層次較低,或特定於某些實現技術的話,提取其介面就有可能達不到預期效果甚至根本就不可行」。
- 因此:「當無法對一個參數的型別使用介面提取,或者當該參數難以『偽裝』的時候,可採用參數適配手法。」
核心做法:把參數型別外覆起來,改讓方法依賴一個自己定義的窄介面,「從而完全解除對 API 介面的依賴」(p.259)。書中的判準是:「介面應傳達職責而非實現細節。這樣的介面令程式碼易於閱讀和維護。」(p.260)
步驟(p.261)
- 建立將被用於該方法的新介面,該介面越簡單且能表達意圖越好。但也要注意,該介面不應導致需要對該方法的程式碼作大規模修改。
- 為新介面建立一個用於產品程式碼的實作。
- 為新介面建立一個用於測試的「偽造」實作。
- 編寫一個簡單的測試案例,將偽物件傳給該方法。
- 對該方法作必要的修改以使其能使用新的參數。
- 執行測試來確保你能使用偽物件來測試該方法。
★★ 在 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),那時走 引入參數物件。