專案做到一半,你是不是也遇過這種情況:表單一改「國家」,「城市」就要清空;清單在載入,按鈕要鎖;API 慢半拍回來,又把新結果覆蓋掉。
元件彼此拉線、互相喊話,最後整個頁面像糾結的耳機線。其實,很多痛苦不是功能難,而是「溝通」散落各處。
Mediator(仲裁者)模式做的事很單純:把互動規則收攏到一個中樞,讓元件各司其職、透過中樞對話。這篇不講玄學,我會用貼近實務的例子(聊天室、表單協作、搜尋流程)帶你看清它怎麼落地、怎麼測、哪裡會踩雷,寫起來更安靜,維護也不再焦慮。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
什麼是 Mediator?為何需要?
定義:建立一個「仲裁者」物件,封裝多個物件間的互動,讓彼此不再互相直接呼叫,只透過仲裁者溝通。這能明顯降低相互依賴,改善可維護性與可測試性。
痛點場景(你可能已經遇過):
表單多個欄位彼此影響(比如國家→城市→郵遞區號),改一處就牽一髮動全身。
頁面上 N 個元件彼此彼此訂閱、彼此呼叫,最後形成蜘蛛網依賴,debug 超痛。
模組之間為了互通塞了一堆事件或 callback,事件風暴難以追蹤。
引入 Mediator 後:每個元件只需要知道仲裁者;仲裁者負責「誰該通知誰、什麼時候做什麼」,溝通規則集中管理,讓變更成本降低。
與 Observer/Pub-Sub 的差異
Observer:一對多「廣播」;被觀察者改變 → 通知訂閱者。
Mediator:多對多「對話協調」;物件間不直連,透過單一協調中心進行雙向或多向互動。
簡言之:Observer 側重通知機制,Mediator 側重互動流程的編排與規則集中化。
很多團隊用 Event Bus(Pub-Sub) 取代一切互動,短期靈活,長期容易變成「誰都在發、誰也在聽」,規則散落各處;Mediator 則把「規則」收斂在中心。兩者可共存,但責任要切清楚。
模式結構與設計要點
角色
Mediator:定義互動協議,接收各參與者的請求,據以轉派或下指令。
Colleague(參與者):只與 Mediator 溝通,不直接互調。
設計要點
保持參與者瘦身,把決策邏輯交給 Mediator。
Mediator 僅負責溝通與流程;商業規則仍可分散在各參與者內部。
當規則變複雜時,把 Mediator 拆成領域中介(多個)或以策略/狀態重構避免「上帝物件」。
實作一:極簡聊天室(Class 版 Mediator)
目標:使用者彼此不直接知道對方,透過 ChatRoomMediator 轉送訊息/廣播。
// 參與者
class User {
constructor(name, mediator) {
this.name = name;
this.mediator = mediator;
mediator.register(this);
}
send(to, text) {
this.mediator.route({ from: this.name, to, text, ts: Date.now() });
}
receive({ from, text, ts }) {
console.log(`[${new Date(ts).toLocaleTimeString()}] ${from} → ${this.name}: ${text}`);
}
}
// 仲裁者
class ChatRoomMediator {
constructor() {
this.users = new Map();
}
register(user) {
if (this.users.has(user.name)) throw new Error(`User ${user.name} already exists`);
this.users.set(user.name, user);
}
route(msg) {
if (msg.to === '*') {
// 廣播
for (const [name, u] of this.users) {
if (name !== msg.from) u.receive(msg);
}
} else {
const target = this.users.get(msg.to);
if (!target) return console.warn(`User ${msg.to} not found.`);
target.receive(msg);
}
}
}
// 使用
const room = new ChatRoomMediator();
const alice = new User('Alice', room);
const bob = new User('Bob', room);
alice.send('Bob', 'Hi!');
bob.send('*', 'Hello everyone!');
重點:User 不知道對方的引用,只知道 mediator。路由邏輯集中在 ChatRoomMediator,好測試、好擴充(如禁言、審核、記錄)。
實作二:前端表單協作(用 EventTarget/CustomEvent 當中介)
瀏覽器原生提供 EventTarget 與 CustomEvent,可作為輕量 Mediator:
各元件向「仲裁者」派發事件;
仲裁者判斷目前狀態後分發動作;
好處:不需額外套件,原生跨瀏覽器支援好(IE 例外)。
場景:地址輸入表單
改「國家」→ 重置「城市」與「郵遞區號」
欄位齊全 → 啟用送出按鈕
// 輕量仲裁者:繼承 EventTarget
class FormMediator extends EventTarget {
constructor() {
super();
this.state = { country: '', city: '', zip: '' };
// 收敘各字段變更
this.addEventListener('field:change', (e) => {
const { name, value } = e.detail;
this.state[name] = value;
// 規則集中:改國家 → 清空城市與郵遞區號
if (name === 'country') {
this.state.city = '';
this.state.zip = '';
this.dispatchEvent(new CustomEvent('ui:update', { detail: { reset: ['city', 'zip'] } }));
}
// 規則集中:是否可送出?
const ready = this.state.country && this.state.city && this.state.zip;
this.dispatchEvent(new CustomEvent('form:ready', { detail: { ready } }));
});
}
}
// 視圖元件只負責回報與接收,不保規則
function bindField(id, name, mediator) {
const el = document.getElementById(id);
el.addEventListener('input', () => {
mediator.dispatchEvent(new CustomEvent('field:change', { detail: { name, value: el.value } }));
});
mediator.addEventListener('ui:update', (e) => {
if (e.detail.reset?.includes(name)) el.value = '';
});
}
function bindSubmit(buttonId, mediator) {
const btn = document.getElementById(buttonId);
mediator.addEventListener('form:ready', (e) => (btn.disabled = !e.detail.ready));
}
// 啟動
const mediator = new FormMediator();
bindField('country', 'country', mediator);
bindField('city', 'city', mediator);
bindField('zip', 'zip', mediator);
bindSubmit('submitBtn', mediator);
上例把「誰該被重置、何時能送出」集中在 FormMediator。CustomEvent.detail 能攜帶自訂資料,事件分發與監聽使用 EventTarget 的 dispatchEvent/addEventListener。
實作三:搜尋流程協調器(API、載入指示、結果清單統一調度)
目標:輸入框、結果清單、載入指示器、API 模組互不耦合,由 SearchMediator 編排完整流程(去抖、併發控制、狀態收斂)。
class SearchMediator {
constructor({ input, resultsView, spinner, api }) {
this.input = input;
this.resultsView = resultsView;
this.spinner = spinner;
this.api = api;
this.timer = null;
this.latestQuery = '';
this.inflightToken = 0;
this.input.onQuery = (q) => this.handleQuery(q);
}
handleQuery(q) {
clearTimeout(this.timer);
this.timer = setTimeout(() => this.search(q), 250); // 簡易 debounce
}
async search(q) {
this.latestQuery = q.trim();
if (!this.latestQuery) {
this.resultsView.render([]);
return;
}
const token = ++this.inflightToken; // 標記最新請求
this.spinner.show();
try {
const list = await this.api.search(this.latestQuery);
if (token !== this.inflightToken) return; // 舊結果丟棄
this.resultsView.render(list);
} catch (err) {
if (token !== this.inflightToken) return;
this.resultsView.error(err);
} finally {
if (token === this.inflightToken) this.spinner.hide();
}
}
}
// --- 假元件 ---
const input = {
onQuery: null,
bind(el) { el.addEventListener('input', () => this.onQuery?.(el.value)); }
};
const resultsView = {
render(list) { /* 更新結果區 */ },
error(e) { /* 顯示錯誤 */ }
};
const spinner = { show(){/*...*/}, hide(){/*...*/} };
const api = { search: (q) => fetch(`/api/search?q=${encodeURIComponent(q)}`).then(r => r.json()) };
// 啟動
const mediator = new SearchMediator({ input, resultsView, spinner, api });
input.bind(document.getElementById('q'));
亮點:
規則(去抖、取消舊請求、Loading 顯示時機)全部在 Mediator;
input/resultsView/spinner/api 都是瘦元件;
測試時可以替換 api 為 mock,或檢查 mediator 的流程是否正確。
常見錯誤與雷點
1. 把 Mediator 寫成「上帝物件」
症狀:所有邏輯都塞進一個巨大類別,動不動上千行。
修正:
按領域切分成多個 mediator(如 FormMediator、SearchMediator)。
將複雜決策抽為策略或規則集合(例如 RuleSet 注入 mediator)。
2. 其實只是廣播需求,卻硬上 Mediator
症狀:邏輯只是單向通知,卻引入仲裁者導致結構過度。
修正:純通知請用 Observer/Pub-Sub;需要流程協調與路由規則再使用 Mediator。
3. 事件風暴(無節制的 CustomEvent)
症狀:事件名稱到處定義,誰 dispatch、誰接都說不清。
修正:
統一事件命名(如 domain:action)。
只允許事件由 mediator 發起或轉發;元件不互相直接派發。
定期做「事件清單」稽核。
4. 記憶體洩漏(未解除監聽)
症狀:元件銷毀後仍持有監聽器。
修正:封裝 subscribe()/dispose(),
在元件 unmount 時呼叫,或使用 AbortController 管理監聽生命週期。
5. 把跨瀏覽器支援想得太美
症狀:在 IE(老舊環境)使用 CustomEvent 出錯。
修正:必要時加 polyfill,或用小型事件匯流排封裝。
6. 異步競賽條件
症狀:舊請求晚回來覆蓋新結果。
修正:加入請求序號或 AbortController;
Mediator 統一丟棄過期結果(見實作三)。
7. 測試困難
症狀:流程跨多元件不好測。
修正:
Mediator 的輸入/輸出可觀察化(事件或回呼);
對外部依賴(API/View)全部注入,測試改用假物件。
8. 命名與邊界不清
症狀:Mediator 夾帶領域規則與 UI 細節,久了混亂。
修正:把「流程協調」與「領域規則」分層,UI 細節留在視圖。
測試策略與重構建議
單元測試(以實作三為例)
注入假 api 與假 resultsView,驗證:
debounce 是否生效(快速輸入只觸發一次搜尋);
舊請求是否被丟棄;
錯誤時是否呼叫 resultsView.error;
spinner.show/hide 的時序是否正確。
契約測試
對各參與者定義最小介面(如 resultsView.render(list)),藉由共同的介面讓替換更安全。
重構指引
當 Mediator 超過某複雜度(例如檔案 >300 行、圈複雜度 >15),先拆副流程到小物件(策略、命令、驗證器),必要時分裂 Mediator。
與其他模式的協同
Facade:
對外提供簡化介面;Mediator 管內部元件互動。兩者常一起用:外部呼 Facade,內部由 Mediator 排程。
Command:
把操作封裝成命令物件;Mediator 只負責何時與下達對象,不碰命令內容。
State/Strategy:
當規則依狀態/策略而變時,把變化封裝再交由 Mediator 切換。
Observer:
Mediator 內部也能用 Observer 作為通知機制,但規則仍在 Mediator 集中。
何時該用/不該用 & 結語
適用:
多元件交互複雜、修改常牽連到其他元件;
想降低耦合、集中互動規則,增進測試與維護性;
前端大型表單、流程編排(搜尋、結帳、預約)、多人協作邏輯(聊天室、遊戲房間)。
不適用:
單純通知、資料單向流(用 Observer/Pub-Sub 更直接);
規模很小、邏輯極簡(直接呼叫最清楚)。
延伸閱讀推薦:
javaScript設計模式 : Factory Method (工廠方法)
javaScript設計模式 : Abstract Factory(抽象工廠)
javaScript設計模式 : Builder(建造者模式)
javaScript設計模式 : Prototype(原型)
javaScript設計模式 : Singleton (單例模式)
javaScript設計模式 : Adapter(轉接器模式)
javaScript設計模式 : Bridge( 橋接模式 )
javaScript設計模式 : Composite(組合模式)
javaScript設計模式 : Decorator(裝飾者)
javaScript設計模式 : Flyweight(享元模式)
javaScript設計模式 : Chain of Responsibility(責任鏈)
javaScript設計模式 : Command Pattern(命令模式)
javaScript設計模式 : Interpreter(直譯器)
javaScript設計模式 : Iterator(迭代器)
javaScript設計模式 : Observer( 觀察者 )
javaScript設計模式 : Strategy(策略模式)
