🎯 什麼情境該想到我

當你「客戶端得先向伺服器物件要出它內部的委託物件,再去呼叫那個委託物件」(例如 person.getDepartment().getManager())的時候——這讓客戶端知道了太多內部結構,內部一改就牽動客戶端。在伺服器類別上加一個轉呼叫方法,把委託藏起來,斬斷這條訊息鏈。

⚙️ 怎麼用(步驟 / 公式)

意圖:在伺服器類別上提供轉呼叫方法,讓客戶端不必知道背後的委託物件。

動作要點(小步安全進行):

  1. 找出客戶端「先取得委託物件、再對它呼叫」的用法。
  2. 在伺服器類別上,為客戶端需要的每個委託操作,建立一個簡單的委託方法(內部自己去呼叫委託物件)。
  3. 逐一把客戶端改成呼叫伺服器上的這個新方法,不再自行取得委託物件,每步測試。
  4. 若都改完後,客戶端已不需要那個取得委託物件的存取器,就移除它,真正把委託隱藏起來。

★ 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 一旦暴露了鏈,收回來就是破壞性變更。

🔗 相關工具