🎯 什麼情境該想到我
當你「看到一串連續的取值呼叫,像 a.getB().getC().getD() 一路點下去」的時候——呼叫端被迫知道整條物件導覽路徑的內部結構。
⚙️ 怎麼用(步驟 / 公式)
這味道是什麼徵兆:訊息鏈讓呼叫端緊緊耦合到物件之間的導覽結構——鏈中任何一環的關係改變,呼叫端就得跟著改。它洩漏了太多內部結構的細節。
通常用哪些重構手法對治:
- 隱藏委託關係(Hide Delegate):在鏈的前端物件上開一個方法,替呼叫端把後面的委託關係藏起來,呼叫端只需認識第一個物件。
- 提煉函式 + 搬移函式(Extract Function + Move Function):把「用這串鏈算出來的東西」抽成一個函式,再把它搬到鏈上更靠近資料的地方。
★ 定義與具體樣貌
// ★ 呼叫端必須知道整條路上每一個中間物件的存在與結構
manager := order.Customer().Department().Manager().Name()
// ↑ 換了 Department 的結構,這行就壞了 —— 而它可能有五十個呼叫點★★ 為什麼會痛:具體情境
// ① 結構改變的波及面
// Department 從 Manager *Person 改成 Managers []*Person
// → 所有 .Department().Manager() 的地方都要改,編譯器會告訴你,但有五十處
// ② ★ nil 在鏈的中間
o.Customer().Department().Manager()
// ↑ Customer 可能是 nil ↑ Department 可能是 nil
// → 要嘛每一層都判 nil(超醜),要嘛等著 panic
// ③ ★ 測試要建整條鏈
// 測「顯示經理名字」這件事,得建 Order → Customer → Department → Person 四個物件★★ 什麼時候這其實不是味道(最重要的一段)
Fowler 自己吐槽德摩特爾法則「該叫偶爾有用的德摩特爾建議」。以下都不是味道:
// ✅ ① 純資料結構的路徑存取 —— 那個形狀就是它的公開契約
cfg.Server.TLS.MinVersion
resp.Request.URL.Host
// ✅ ② 流暢介面(fluent / builder)—— 刻意設計成鏈式
strings.NewReplacer("-", "").Replace(s)
req.WithContext(ctx)
qb.Where("a = ?", 1).OrderBy("b").Limit(10)
// ✅ ③ 標準庫的正常用法
resp.Body.Close()
tx.QueryRowContext(ctx, q).Scan(&v)
// ✅ ④ 只有一、兩個呼叫點
// 包一個轉發方法的維護成本,高於直接寫這條鏈★★ 判準:中間那幾層是「你在依賴的實作細節」,還是「本來就是公開的資料形狀」?
cfg.Server.TLS——Server、TLS就是 config 的形狀,藏起來要寫三十個轉發方法,純虧。
order.Customer().Department()——Department是Customer的內部組織方式,呼叫端不該知道。
★ 對治的三條路(不是只有「包起來」)
| 做法 | 什麼時候 | 代價 |
|---|---|---|
| 隱藏委託關係 | 鏈的終點是「取一個值」,且呼叫點多 | ★ 轉發方法太多會變成 中間人 |
| ★ 搬移函式 | 你在鏈的終點「做事」,不只是取值 | 常常是最好的解 |
| 什麼都不做 | 一、兩個呼叫點;或中間層是公開資料形狀 | — |
// ★ 第二條路的例子 —— 這才是最好的解
// before:呼叫端鑽進去做事
if order.Customer().Department().Manager().Level >= 3 { approve() }
// after:把「誰能核准」變成它該在的地方的責任
if order.CanBeApprovedAutomatically() { approve() }
// ★ 鏈消失了,而且業務規則有了名字(可以被測試、可以被改)★ 鏈的出現通常表示「呼叫端在做一件本該由別人做的事」——
那是 依戀情結,而 搬移函式 才是根治。包一層轉發只是把鏈藏起來。
★ Go 的三個特殊細節
// ① ★ Go 沒有 ?. —— 鏈上每一層的 nil 都要自己處理
// 但有一個漂亮的替代:nil 接收者上的方法(見 [[引入特例]])
func (d *Department) ManagerName() string {
if d == nil { return "" } // ★ 鏈上的 nil 由鏈自己吸收
return d.manager.Name()
}
// ② ★ 方法鏈 vs 欄位鏈
o.Customer().Department() // ★ 方法鏈:可以在任一層加 nil 保護與邏輯
o.Customer.Department // ★ 欄位鏈:完全無法攔截 —— 這種更該處理
// ③ 錯誤怎麼在鏈裡傳?Go 沒有 Result 型別
// → 這是 Go 不適合長鏈的根本原因;鏈上任何一步可能出錯時,就不要用鏈★ 所以在 Go 裡優先處理「欄位鏈」而不是「方法鏈」:
欄位鏈沒有任何攔截點,改結構就是全面波及。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
別為了消滅每一個點號而把大量委託方法轉進中間物件,那會走向「中間人」味道——要拿捏。
-
流暢介面(fluent API)刻意的鏈式呼叫不是此味道。
-
★★ 純資料結構的路徑存取不是味道(
cfg.Server.TLS.MinVersion)。硬包會產生幾十個無意義的轉發方法。 -
★ 流暢介面/builder 是刻意設計的,
qb.Where().OrderBy().Limit()不要「修」。 -
★ 包轉發方法只是把鏈藏起來。如果你在鏈的終點「做事」而不只是取值,根治是 搬移函式。
-
轉發方法超過三、四個就會變成 中間人——那時該直接暴露委託物件。
-
★ Go 裡優先處理「欄位鏈」,它沒有攔截點;方法鏈至少還能在中間加保護。
-
Go 沒有
?.,鏈上的 nil 要自己吸收。nil 接收者上的方法是最乾淨的做法。 -
只有一兩個呼叫點就別動。包一層的成本高於收益。
🔗 相關工具
- 中間人 Middle Man(過度隱藏委託會走向另一極端)
- 內幕交易 Insider Trading(訊息鏈是模組間過度了解彼此結構的一種形式)
- 重構
- 隱藏委託關係 —— 直接的對治手法
- 搬移函式 —— ★ 根治:把事情搬到它該做的地方
- 中間人 —— 包過頭的反效果
- 依戀情結 —— 鏈的根因常常是它
- 引入特例 —— 用 nil 接收者吸收鏈上的 nil