🎯 什麼情境該想到我

當你「一群物件彼此互相呼叫,連線數已經爆炸成 」的時候——
表單元件互相影響(選了 A 就要禁用 B)、聊天室、多人協作、UI 元件協調。

⚙️ 怎麼用

意圖

用一個中介物件來封裝一系列物件的互動。中介者使各物件不需要顯式地互相引用,從而使其耦合鬆散。

❌ 沒有中介者:n 個物件兩兩相連 → O(n²) 條關係
   A ⇄ B ⇄ C
   ⇅ ╳  ⇅       每加一個元件,要改所有既有元件
   D ⇄ E ⇄ F

✅ 有中介者:n 條
   A   B   C
    ╲  │  ╱
     Mediator     ★ 每個元件只認識中介者
    ╱  │  ╲
   D   E   F

★ 和 Observer 的差別(必考)

中介者觀察者模式
中心知不知道成員★ 知道(它要協調它們)❌ 不知道(Subject 不認識訂閱者)
方向★ 多對多(同儕之間)一對多(Subject → Observers)
中心的角色★ 有邏輯(決定誰該做什麼)只是轉發(沒有業務邏輯)
成員之間互不認識,透過中介者Observer 也不認識彼此

★ 一句話:觀察者的中心是「啞的廣播器」,中介者的中心是「聰明的協調者」。
如果你的「事件匯流排」開始包含「收到 X 事件時要去做 Y」的邏輯,它已經是中介者了。

★ Go 的長相

// 元件只認識中介者
type Component interface {
    SetMediator(Mediator)
}
 
type Mediator interface {
    Notify(sender Component, event string)
}
 
// ★ 協調邏輯全部集中在這裡
type dialog struct {
    checkbox *Checkbox
    textbox  *TextBox
    submit   *Button
}
 
func (d *dialog) Notify(sender Component, event string) {
    switch {
    case sender == d.checkbox && event == "toggled":
        d.textbox.SetEnabled(d.checkbox.Checked())      // ★ 元件之間的規則在這
        d.revalidate()
    case sender == d.textbox && event == "changed":
        d.revalidate()
    }
}
 
func (d *dialog) revalidate() {
    d.submit.SetEnabled(!d.checkbox.Checked() || d.textbox.Text() != "")
}

★ 價值在於:Checkbox 完全不知道 TextBox 和 Button 存在。
規則「勾選時才啟用文字框,且文字框非空才能送出」在一個地方讀得到。

★ Go 更慣用的形態:一個協調函式 / channel hub

// ★ 聊天室 hub —— Go 的經典中介者
type Hub struct {
    clients    map[*Client]bool
    broadcast  chan Message
    register   chan *Client
    unregister chan *Client
}
 
func (h *Hub) Run() {
    for {
        select {                                   // ★ 單一 goroutine 序列化所有狀態變更
        case c := <-h.register:
            h.clients[c] = true
        case c := <-h.unregister:
            delete(h.clients, c)
            close(c.send)
        case m := <-h.broadcast:
            for c := range h.clients {
                select {
                case c.send <- m:
                default:                           // ★ 慢的 client 直接踢掉
                    delete(h.clients, c)
                    close(c.send)
                }
            }
        }
    }
}

★ 這是 Go 版 WebSocket 聊天室的標準骨架(gorilla/websocket 的官方範例就長這樣)。
用「單一 goroutine + select」序列化所有狀態變更,完全不需要 mutex
——這正是 工具-靠通訊共享記憶體 的精神。

TypeScript 的長相

// 在 TS 裡,中介者常常就是一個 store / reducer
type Action =
  | { type: "toggleAdvanced" }
  | { type: "setName"; value: string };
 
function reducer(state: FormState, action: Action): FormState {
  switch (action.type) {
    case "toggleAdvanced": {
      const advanced = !state.advanced;
      // ★ 元件之間的規則集中在這
      return { ...state, advanced, canSubmit: !advanced || state.name !== "" };
    }
    case "setName":
      return { ...state, name: action.value, canSubmit: !state.advanced || action.value !== "" };
  }
}

★ Redux 的 store 就是一個中介者——元件只 dispatch,不互相呼叫。
這也解釋了為什麼前端狀態管理會演化成這個形狀:它解的正是 的元件耦合問題。

🧪 我實際套用的紀錄

  • (待填)

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

  • ★ 最大的風險:中介者變成上帝物件。
    所有互動邏輯都集中過來 → 它變成一個幾千行、什麼都知道的怪物。
    你把 的複雜度從「元件之間」搬到了「中介者內部」,沒有消滅它。
    → 對策:
    • 按領域拆成多個中介者(表單一個、導航一個),不要一個管全部
    • 中介者只放協調邏輯,業務規則下放到領域物件
    • 超過幾百行就該拆
  • ★ 元件數少(2–3 個)不需要中介者。直接互相引用更簡單清楚。
    它的價值在 夠大、且關係夠密集的時候。
  • 除錯:所有互動都經過中介者,堆疊裡看不到「誰觸發了誰」。要 log。
  • 中介者本身成為單點:它掛了所有協調都停擺;它變慢所有互動都變慢。
  • 不要把中介者當成事件匯流排用。中介者知道它的成員;事件匯流排不知道。
    兩者混用會得到「既不知道誰在聽、又塞滿業務邏輯」的最糟組合。
  • 並行下的中介者要序列化(Go 用單一 goroutine + channel,見上;或用 mutex)。

🔗 相關工具