🎯 什麼情境該想到我
當你「想換掉某個方法內部自己建立出來的那個物件,好做感知或分離」的時候。
⚙️ 怎麼用(步驟)
書的設定:假設你有一個方法,該方法在內部建立了某個物件,而你想要透過替換該物件來實現感知或分離。往往最簡單的辦法就是從外面將你的物件傳進來(p.301)。
書中另外點出一個保命技巧:借助於一點函式轉發技巧,我們就可以完全保留原來的那個函式簽名——把原來那個無參數的版本改成只做一件事,即建立一個物件並呼叫參數化之後的版本(p.301)。
書中列的步驟(p.302):
- 找出目標方法,將它複製一份。
- 給其中一份增加一個參數,並將方法體中相應的物件建立語句去掉,改為使用剛增加的這個參數。
- 將另一份複製的方法體刪掉,代以對被參數化了的那個版本的呼叫,記得建立相應的物件作參數。
命名的取捨(p.301):C++、Java、C# 以及許多其他語言都允許同一個類別上有多個同名方法,前提是它們的簽名各不相同。書中的例子就利用了這個便利,使原方法和參數化之後的方法具有同樣的名字。「儘管這省了點事,但有時也會帶來混亂。一個替代方案是將新參數的型別名嵌入方法名中」——例如原方法名保留不變,把加了參數的那個版本改叫「帶某型別參數的某方法」。
★★ 在 Go 裡:它有一個比「解依賴」更重要的用途
★ 書中的用途:★★ 把方法內部建立的物件改成參數,★ 這樣測試可以傳假的進去
★★ Go 裡的第一用途:★ 把「不確定性」變成參數 ⇒ 讓函式變成純函式
// ❌★★ 方法內部取得「現在」與「新 ID」⇒ 不可測、不可重現
func (o *Order) Cancel(reason string) error {
o.cancelledAt = time.Now() // ★★ 每次跑結果不同
o.events = append(o.events, Event{ID: uuid.NewString()}) // ★★ 同上
}
// ✅ ★ 參數化 ⇒ ★★ 同樣輸入必定同樣輸出
func (o *Order) Cancel(reason string, at time.Time, eventID string) error { … }★★ 但參數會變多,所以實務上有兩種折衷:
// ★ ① 把不確定性收在型別的欄位上(★ 見 [[換函式為函式指標]])
type Order struct{ now func() time.Time; newID func() string }
// → ★ 呼叫端的簽章保持乾淨,★★ 而測試在建構時注入
// ★★ ② 把「一次操作的所有不確定性」包成一個參數
type Clock struct{ Now time.Time; NewID func() string }
func (o *Order) Cancel(reason string, c Clock) error { … }
// → ★ 簽章只多一個參數,★★ 而且「這個操作依賴什麼不確定性」一目瞭然★ 判準:★★ 這個型別的「多數方法」都需要它嗎? 都需要 → ① 欄位;只有一兩個 → ② 參數。
★★ 一個 Go 特有的重要用途:把 tx 參數化
// ❌★★ Repo 持有 tx ⇒ ★ 那個 Repo 變成 per-request 的
type Repo struct{ tx *sql.Tx }
// ✅ ★ tx 當參數 ⇒ ★★ Repo 可以是單一實例,而「有沒有在交易裡」看得見
type Repo struct{}
func (r *Repo) Get(ctx context.Context, q db.Querier, id ID) (*Order, error)
func (r *Repo) Save(ctx context.Context, q db.Querier, o *Order) error★★ 這是 Go 資料層最重要的一次「參數化方法」:
★ 它讓同一個 repo 可以用在交易內或交易外(見 表格資料閘道 的 DBTX 介面)。
★★ 對照的反模式:★ 把
tx放 struct 或放ctx。
★ 放 struct → per-request 物件;★★ 放 ctx → 「有沒有在交易裡」完全看不出來。
★★ 什麼時候「不要」參數化
① ★★ 參數已經超過 4 個
→ ★ 先做 [[引入參數物件]],★★ 而不是繼續加
② ★★ 那個依賴「每次呼叫都一樣」
→ ★ 它該是欄位(★ 見 [[依賴下推]] 的判準)
③ ★ 參數化之後呼叫端要「憑空生出」一個值
★ func Process(o Order, internalCache *cache) → ★★ 呼叫端不該知道 cache 存在
→ ★ 那是實作細節,該留在型別內部
④ ★★ 它讓呼叫端可以傳進「不合法的組合」
★ func Ship(o Order, status Status) → ★★ 呼叫端可以傳任何 status
→ ★ 那不是參數化,是把不變式交給呼叫端(★ 見 [[實體]])
★★ ④ 是最危險的:★ 「為了可測」而把狀態轉移變成參數
→ 等於把不變式的守點移出了領域模型。
★ 判準:★★ 呼叫端有沒有辦法傳進一個「不可能」的值? 有 → 參數化錯了。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
會擴散型別依賴,且書給了替代方案:「與參數化建構函式手法一樣,參數化方法也可能會導致客戶程式碼依賴於新出現的那個參數的型別。如果你覺得這的確會帶來問題的話,可以考慮改用提取並重寫工廠方法」(p.302)。
-
同名多載可能造成混亂:見上「命名的取捨」——省事與可讀性之間要自己選(p.301)。
-
與《重構》的分界:外觀上很像《重構》的「加入參數/改變函式宣告」,但動機完全不同:這裡不是為了讓介面更好,而是為了在測試中把物件從外面換進去,好進行感知或分離。書甚至教你用轉發技巧把原簽名原封不動保留下來——正因為目的只是開一道測試用的門,不是改設計。
-
★★ Go 裡的第一用途是「把不確定性變成參數」(時間、ID、隨機),讓函式變成純函式而非只是為了解依賴。
-
★★ 最重要的一次應用是把
tx參數化,讓同一個 repo 能用在交易內或外。放 struct → per-request 物件;放ctx→ 「有沒有在交易裡」看不出來。 -
★★ 不要為了可測而把狀態轉移變成參數(
Ship(o, status))——那等於把不變式的守點移出領域模型。 -
★ 判準:呼叫端有沒有辦法傳進一個「不可能」的值? 有 → 參數化錯了。
-
★ 判準:這個型別的多數方法都需要它嗎? 都需要 → 欄位;只有一兩個 → 參數。
-
★ 參數超過 4 個先做 引入參數物件,不要繼續加。
-
★ 不要把實作細節(內部快取)變成參數,呼叫端不該知道它存在。