🎯 什麼情境該想到我

當你「打開一個類別,發現它大部分的方法都只是把呼叫原封不動轉交給另一個物件」的時候——它自己幾乎不做事,只當轉手。

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

這味道是什麼徵兆:封裝與委託是好東西,但過了頭就變成問題——當一個類別半數以上的方法都只是委派給別人,這層中間人就只剩間接的成本、沒帶來價值。

通常用哪些重構手法對治:

  • 移除中間人(Remove Middle Man):讓呼叫端直接與真正做事的物件對話,拿掉多餘轉手。
  • 以繼承取代委託 / 以委託取代子類(Replace Superclass with Delegate / Replace Subclass with Delegate):視關係本質,在繼承與委託之間調整結構。
  • 若只有少數幾個委派方法麻煩,可內聯函式把它們併掉。

★ 定義與怎麼具體認出來

// ★ 訊號:一個型別的方法,一半以上只是「轉發給同一個委託物件」
type Person struct{ department *Department }
 
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 的傳聲筒

可量化的判準:

# ★ 數「一行 return x.field.Something 的方法」佔比
grep -cE '^\s*return [a-z]+\.[a-z]+\.[A-Za-z]+(\(.*\))?$' --include='*.go' -r .

★ 經驗門檻:轉發方法佔一個型別方法總數的一半以上,就是這個味道。

★★ 它的來源:隱藏委託關係 做過頭

[[訊息鏈]](暴露太多)  ──隱藏委託關係──▶  ★ 剛好  ──再包更多──▶  中間人(隱藏太多)

★ 這兩個味道是同一條軸的兩端,而「剛好」的位置會隨需求移動。
所以不要追求「一次調對」——它會來回擺動,那是正常的。

判準:轉發方法的數量。
一、兩個 → 你在保護一個實作細節,這是對的。
十幾個 → 你其實只是想暴露那個委託物件,那就大方暴露(反向操作)。

★★ Go 特有:這五種「中間人」是設計,不是味道

// ① ★ 裝飾者 / middleware —— 轉發就是它的工作
type loggingStore struct {
    Store                                    // ★ 嵌入介面:其餘方法自動轉發
    log *slog.Logger
}
func (s loggingStore) Put(ctx context.Context, k string, v []byte) error {
    s.log.Info("put", "key", k)
    return s.Store.Put(ctx, k, v)            // ★ 只覆寫這一個,其餘全轉發 —— 這是 [[裝飾者模式]]
}
 
// ② ★ 配接器 —— 存在的意義就是轉換與轉發([[配接器模式]])
// ③ ★ 外觀 —— 為複雜子系統提供簡化入口([[外觀模式]])
// ④ ★ 代理 —— 快取、延遲載入、權限檢查([[代理模式]])
// ⑤ ★★ 防腐層 —— 刻意把外部模型隔在邊界外([[防腐層]])

★★ 這五種的轉發都是「有理由的」:它們在轉發的同時做了一件事
(記 log、轉換型別、簡化介面、加快取、隔離外部模型)。
純粹的中間人是「什麼都沒做」的轉發。

★ Go 的嵌入讓這件事更微妙:

type Service struct{ *Repo }     // ★ 一行就轉發了 Repo 的所有匯出方法

這不是中間人味道(沒有樣板程式碼要維護),但它有另一個問題:
Repo 新增一個匯出方法 → Service 的公開 API 自動變大,你沒得選。
→ 見 工具-Go嵌入機制。

★ 對治手法

情況對治
一半以上的方法是純轉發★ 公開委託物件(p.Department()),刪掉轉發方法
整個型別只是轉發內聯類別——把它合併掉
轉發的同時應該做事搬移函式——把邏輯搬進來,它就不再是中間人
只有幾個轉發但很囉唆Go 的嵌入(⚠️ 代價是 API 全部攤出來)

⚠️ 移除中間人的代價

公開委託物件 = 把它變成公開契約。 之後想換掉 Department 的型別,
就是破壞性變更了——而原本有轉發層時,你可以自由替換。
★ 所以「有幾個轉發方法」不是唯一判準,還要問「這個委託物件穩定嗎」:
不穩定(外部套件、會換的實作)→ ★ 保留轉發層是對的,即使方法很多(那就是 防腐層)。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 委託本身不是壞事——為了隱藏委託(避免訊息鏈)而保留少量轉發是合理的。

  • 中間人與訊息鏈是一體兩面:修訊息鏈可能製造中間人,反之亦然,要在兩者間取平衡點。

  • ★★ 裝飾者、配接器、外觀、代理、防腐層都是「刻意的中間人」,不是味道。它們在轉發的同時做了一件事。

  • ★ 委託物件不穩定時(外部套件、會替換的實作),保留大量轉發是對的——那是防腐層。

  • ★ 公開委託物件 = 把它變成公開契約,之後換不掉。移除中間人不是免費的。

  • Go 的嵌入一行就轉發全部方法,沒有樣板成本——但代價是被嵌入型別新增方法時,你的 API 自動變大。

  • 這個味道和 訊息鏈 是同一條軸的兩端,「剛好」的位置會隨需求移動。不要追求一次調對。

  • 判準不只是數量:五個「什麼都沒做」的轉發是味道;二十個「隔離外部模型」的轉發是設計。

🔗 相關工具