🎯 什麼情境該想到我
當你「客戶端得先向伺服器物件要出它內部的委託物件,再去呼叫那個委託物件」(例如 person.getDepartment().getManager())的時候——這讓客戶端知道了太多內部結構,內部一改就牽動客戶端。在伺服器類別上加一個轉呼叫方法,把委託藏起來,斬斷這條訊息鏈。
⚙️ 怎麼用(步驟 / 公式)
意圖:在伺服器類別上提供轉呼叫方法,讓客戶端不必知道背後的委託物件。
動作要點(小步安全進行):
- 找出客戶端「先取得委託物件、再對它呼叫」的用法。
- 在伺服器類別上,為客戶端需要的每個委託操作,建立一個簡單的委託方法(內部自己去呼叫委託物件)。
- 逐一把客戶端改成呼叫伺服器上的這個新方法,不再自行取得委託物件,每步測試。
- 若都改完後,客戶端已不需要那個取得委託物件的存取器,就移除它,真正把委託隱藏起來。
★ before / after(Go)
// before:呼叫端必須知道 Person → Department → Manager 這整條鏈
type Person struct{ Department *Department }
type Department struct{ Manager *Person; ChargeCode string }
manager := john.Department.Manager // ★ 呼叫端依賴了 Department 的存在與結構
// after:Person 把 Department 藏起來,只暴露呼叫端真正要的東西
type Person struct{ department *Department } // ★ 小寫,套件外看不到
func (p *Person) Manager() *Person { return p.department.Manager }現在 Department 換結構、改名、甚至拆成兩個型別,呼叫端一行都不用改。
★ 這是「德摩特爾法則」的具體操作
只跟你的朋友說話 —— 一個方法只該呼叫:自己的方法、自己欄位的方法、參數的方法、自己建立的物件的方法。
不該是a.B().C().D()這種一路鑽進去。
Fowler 自己吐槽這條法則「該叫『偶爾有用的德摩特爾建議』」——它是訊號,不是規則。
★★ 什麼時候是誤判:鏈不等於壞味道
// ✅ 這些都不是訊息鏈,不要「修」它們
strings.NewReplacer("-", "").Replace(s) // 流暢介面(fluent),刻意設計的
req.WithContext(ctx).Header.Set("X-A", "b") // 標準庫的正常用法
cfg.Server.TLS.MinVersion // ★ 純資料結構的路徑存取
resp.Body.Close() // Body 就是 resp 的公開契約★ 判準:中間那一層是「你在依賴的實作細節」,還是「本來就是公開資料結構」?
cfg.Server.TLS.MinVersion 裡的 Server、TLS 就是 config 的形狀,藏起來只會多寫三十個轉發方法。
⚠️ 藏得太多就變成 中間人
// ★ 這是隱藏委託關係做過頭的樣子
func (p *Person) ChargeCode() string { return p.department.ChargeCode }
func (p *Person) Manager() *Person { return p.department.Manager }
func (p *Person) Budget() int { return p.department.Budget }
func (p *Person) HeadCount() int { return p.department.HeadCount }
func (p *Person) Location() string { return p.department.Location }
// … 再十個Person 有一半的方法只是在轉發 Department → 反向操作:把 Department() 公開,讓呼叫端直接拿(見 中間人)。
★ 判準:轉發方法的數量。
一、兩個 → 隱藏是對的(你在保護一個實作細節)。
十幾個 → 你其實只是想暴露 Department,那就大方暴露。
★ Go 的第三條路:嵌入(但要小心)
type Person struct {
*Department // ★ 嵌入 —— Department 的所有匯出方法自動出現在 Person 上
Name string
}
john.Manager() // 直接可用,不必寫轉發⚠️ 這不是「隱藏」,是「全部暴露」:
Department新增一個匯出方法 →Person的 API 自動變大,你沒得選。john.Department這個欄位名仍然可以直接存取(嵌入欄位的名字就是型別名)→ 鏈根本沒被藏。- 嵌入指標時
Department是 nil →john.Manager()panic,而且堆疊看起來像是Person的問題。
→ 要隱藏就用小寫具名欄位 + 明確轉發。嵌入是為了「省樣板」,不是為了封裝(見 工具-Go嵌入機制)。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若很多客戶端需要委託物件的大量不同方法,伺服器會被迫長出一堆轉呼叫方法(形成「中間人」壞味道);這時反而該反向做「移除中間人」,讓客戶端直接使用委託物件。
-
對治的壞味道:訊息鏈(Message Chains)。
-
反向手法:移除中間人(Remove Middle Man)。
-
★ 純資料結構的路徑存取不是訊息鏈(
cfg.Server.TLS.MinVersion)。硬包會產生幾十個無意義的轉發方法。 -
★ 流暢介面(fluent API)是刻意設計的,
b.Where(…).OrderBy(…).Limit(…)不要「修」。 -
★ 轉發方法超過三、四個就該反省:你可能該直接暴露那個委託物件,或者是委託方向錯了(該做 搬移函式,把邏輯搬到
Department上)。 -
Go 的嵌入不是隱藏:它把被嵌入型別的所有匯出方法都攤出來,而且嵌入欄位名仍可直接存取。
-
每個轉發方法都是一筆維護成本:
Department的方法改簽章,Person要跟著改。 -
這個手法只在「你控制得住呼叫端」時划算。對外 API 一旦暴露了鏈,收回來就是破壞性變更。
🔗 相關工具
- 提煉類別 Extract Class(拆出新類別後常搭配此手法隱藏委託)
- 封裝變數 Encapsulate Variable(同屬「隱藏內部、控管存取」的精神)
- 重構
- 訊息鏈 —— 觸發本手法的壞味道
- 中間人 —— 做過頭的反效果
- 搬移函式 —— 另一條路:把邏輯搬到資料所在的型別上
- 依戀情結 —— 「一路鑽進去取資料」常常是這個味道
- 工具-Go嵌入機制 —— 嵌入為什麼不是封裝
- 工具-介面設計原則 —— 該暴露多少