跳到主要内容

中介者模式

问题

什么是中介者模式?它解决了什么问题?前端开发中有哪些典型的中介者模式应用?中介者模式和观察者模式、发布订阅模式有什么区别?

答案

中介者模式(Mediator Pattern)是 GoF 23 种经典设计模式之一,属于行为型模式。它通过引入一个中介者对象来封装一组对象之间的交互,使得各对象之间不再直接引用,从而实现松耦合

GoF 定义:定义一个对象来封装一组对象之间的交互。中介者通过使对象之间不需要显式地相互引用,从而使其耦合松散,并且可以独立地改变它们之间的交互。


核心概念与角色

中介者模式包含两个核心角色:

角色说明
Mediator(中介者)定义与各 Colleague 通信的接口,协调各 Colleague 之间的交互逻辑
Colleague(同事/参与者)每个参与者只知道中介者,不直接与其他参与者通信
核心思想

中介者模式的本质是将网状的多对多通信转化为星型的一对多通信,所有交互逻辑都集中到中介者中管理。


网状拓扑 vs 星型拓扑

没有中介者时,N 个组件之间的通信是网状的,每个组件都需要知道其他组件的存在:

引入中介者后,通信变为星型拓扑,所有组件只与中介者通信:

连接数对比
  • 无中介者:N 个组件需要 N×(N1)/2N \times (N-1) / 2 条连接(如 4 个组件需 6 条连接,10 个组件需 45 条连接)
  • 有中介者:N 个组件只需要 NN 条连接(每个组件只连接中介者)

TypeScript 实现 — 聊天室示例

聊天室是中介者模式最经典的例子:ChatRoom 作为中介者,User 作为 Colleague。

mediator-chatroom.ts
// ========== 中介者接口 ==========
interface ChatMediator {
register(user: ChatUser): void;
sendMessage(message: string, sender: ChatUser, receiver?: ChatUser): void;
}

// ========== 同事接口 ==========
interface ChatUser {
name: string;
mediator: ChatMediator;
send(message: string, receiver?: ChatUser): void;
receive(message: string, sender: ChatUser): void;
}

// ========== 具体中介者:聊天室 ==========
class ChatRoom implements ChatMediator {
private users: Map<string, ChatUser> = new Map();

register(user: ChatUser): void {
this.users.set(user.name, user);
console.log(`[ChatRoom] ${user.name} 加入了聊天室`);
}

sendMessage(message: string, sender: ChatUser, receiver?: ChatUser): void {
if (receiver) {
// 私聊:只发给指定用户
receiver.receive(message, sender);
} else {
// 群发:发给除发送者外的所有用户
this.users.forEach((user) => {
if (user !== sender) {
user.receive(message, sender);
}
});
}
}
}

// ========== 具体同事:用户 ==========
class User implements ChatUser {
constructor(
public name: string,
public mediator: ChatMediator
) {
// 注册到中介者
this.mediator.register(this);
}

send(message: string, receiver?: ChatUser): void {
console.log(`[${this.name}] 发送: ${message}`);
this.mediator.sendMessage(message, this, receiver);
}

receive(message: string, sender: ChatUser): void {
console.log(`[${this.name}] 收到来自 ${sender.name} 的消息: ${message}`);
}
}

// ========== 使用示例 ==========
const chatRoom = new ChatRoom();
const alice = new User('Alice', chatRoom);
const bob = new User('Bob', chatRoom);
const charlie = new User('Charlie', chatRoom);

alice.send('大家好!');
// [Alice] 发送: 大家好!
// [Bob] 收到来自 Alice 的消息: 大家好!
// [Charlie] 收到来自 Alice 的消息: 大家好!

bob.send('你好 Alice!', alice);
// [Bob] 发送: 你好 Alice!
// [Alice] 收到来自 Bob 的消息: 你好 Alice!
关键点
  • User 不直接引用其他 User,只通过 ChatRoom 中介者通信
  • 新增用户只需注册到 ChatRoom,无需修改已有用户的代码
  • 所有消息路由逻辑集中在 ChatRoom 中管理

前端实际应用

1. 表单联动 — FormMediator

复杂表单中,多个表单控件之间存在联动关系(如省市区三级联动、勾选协议才能提交等)。如果控件之间直接通信,耦合度极高。引入 FormMediator 来协调所有联动逻辑:

form-mediator.ts
// 表单控件接口
interface FormControl {
name: string;
value: unknown;
setDisabled(disabled: boolean): void;
setValue(value: unknown): void;
}

// 表单中介者
class FormMediator {
private controls: Map<string, FormControl> = new Map();
private rules: Array<(changed: string, controls: Map<string, FormControl>) => void> = [];

register(control: FormControl): void {
this.controls.set(control.name, control);
}

addRule(rule: (changed: string, controls: Map<string, FormControl>) => void): void {
this.rules.push(rule);
}

// 某个控件值变化时,通知中介者执行联动规则
notify(controlName: string): void {
this.rules.forEach((rule) => rule(controlName, this.controls));
}
}

// 使用示例:省市区三级联动
const mediator = new FormMediator();

// 注册控件
mediator.register({ name: 'province', value: '', setDisabled() {}, setValue(v) { this.value = v; } });
mediator.register({ name: 'city', value: '', setDisabled() {}, setValue(v) { this.value = v; } });
mediator.register({ name: 'district', value: '', setDisabled() {}, setValue(v) { this.value = v; } });

// 添加联动规则
mediator.addRule((changed, controls) => {
if (changed === 'province') {
// 省份变化时,清空市和区
controls.get('city')!.setValue('');
controls.get('district')!.setValue('');
}
if (changed === 'city') {
// 城市变化时,清空区
controls.get('district')!.setValue('');
}
});

// 当省份控件值变化时
mediator.notify('province');

2. Vue EventBus / mitt — 事件中心即中介者

Vue 中常用的 EventBus(或第三方库 mitt)本质上就是一个中介者:

event-bus-mediator.ts
import mitt from 'mitt';

// mitt 创建的事件中心就是中介者
type Events = {
'cart:add': { productId: string; quantity: number };
'cart:remove': { productId: string };
'cart:update': { items: Array<{ productId: string; quantity: number }> };
};

const bus = mitt<Events>(); // 中介者

// 组件 A:商品列表(Colleague)
function ProductList() {
function handleAddToCart(productId: string) {
// 不直接调用购物车组件,而是通过中介者通知
bus.emit('cart:add', { productId, quantity: 1 });
}
}

// 组件 B:购物车(Colleague)
function ShoppingCart() {
bus.on('cart:add', ({ productId, quantity }) => {
// 处理添加商品逻辑
console.log(`添加商品 ${productId},数量 ${quantity}`);
// 更新后通知其他组件
bus.emit('cart:update', { items: [/* ... */] });
});
}

// 组件 C:顶部导航栏-购物车图标(Colleague)
function CartBadge() {
bus.on('cart:update', ({ items }) => {
// 更新购物车数量角标
console.log(`购物车共 ${items.length} 件商品`);
});
}
注意

EventBus 在 Vue 3 中已被移除(不再推荐 new Vue() 作为事件总线)。Vue 3 推荐使用 mittPiniaprovide/inject。更多内容参考 Vue 组件通信方式

3. React Context 作为中介者

React 的 Context 可以充当中介者角色,在组件树中共享状态和方法,子组件通过 Context 间接通信:

react-context-mediator.tsx
import { createContext, useContext, useReducer, type ReactNode, type Dispatch } from 'react';

// 定义状态与动作
interface ThemeState {
mode: 'light' | 'dark';
fontSize: number;
}

type ThemeAction =
| { type: 'TOGGLE_MODE' }
| { type: 'SET_FONT_SIZE'; payload: number };

function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
switch (action.type) {
case 'TOGGLE_MODE':
return { ...state, mode: state.mode === 'light' ? 'dark' : 'light' };
case 'SET_FONT_SIZE':
return { ...state, fontSize: action.payload };
default:
return state;
}
}

// Context 充当中介者
const ThemeContext = createContext<{
state: ThemeState;
dispatch: Dispatch<ThemeAction>;
} | null>(null);

function ThemeProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(themeReducer, { mode: 'light', fontSize: 14 });
return (
<ThemeContext.Provider value={{ state, dispatch }}>
{children}
</ThemeContext.Provider>
);
}

function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}

// 子组件 A(Colleague): 主题切换按钮
function ThemeToggle() {
const { state, dispatch } = useTheme();
return (
<button onClick={() => dispatch({ type: 'TOGGLE_MODE' })}>
当前: {state.mode}
</button>
);
}

// 子组件 B(Colleague): 字体大小调节
function FontSizeSlider() {
const { state, dispatch } = useTheme();
return (
<input
type="range"
min={12}
max={24}
value={state.fontSize}
onChange={(e) => dispatch({ type: 'SET_FONT_SIZE', payload: Number(e.target.value) })}
/>
);
}
信息

更多 React 组件通信方案参考 React 组件通信方案

4. 微前端应用间通信 — GlobalMediator

在微前端架构中,多个子应用之间需要通信(如登录状态同步、路由协调等),使用全局中介者避免子应用之间的直接依赖:

micro-frontend-mediator.ts
// 全局中介者
class GlobalMediator {
private apps: Map<string, MicroApp> = new Map();
private handlers: Map<string, Array<(data: unknown, source: string) => void>> = new Map();

register(appName: string, app: MicroApp): void {
this.apps.set(appName, app);
console.log(`[GlobalMediator] 子应用 ${appName} 已注册`);
}

unregister(appName: string): void {
this.apps.delete(appName);
}

on(event: string, handler: (data: unknown, source: string) => void): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event)!.push(handler);
}

emit(event: string, data: unknown, source: string): void {
const handlers = this.handlers.get(event) || [];
handlers.forEach((handler) => handler(data, source));
}
}

interface MicroApp {
name: string;
mount(): void;
unmount(): void;
}

// 挂载到全局(主应用中初始化)
const mediator = new GlobalMediator();
(window as any).__GLOBAL_MEDIATOR__ = mediator;

// 子应用 A:用户模块
mediator.on('user:login', (data, source) => {
console.log(`[子应用A] 收到来自 ${source} 的登录事件`, data);
});

// 子应用 B:触发登录
mediator.emit('user:login', { userId: '123', token: 'abc' }, '子应用B');

5. MVC 中的 Controller 角色

在 MVC(Model-View-Controller)架构中,Controller 本质上就是 Model 和 View 之间的中介者:

  • View 不直接修改 Model,而是将用户操作通知给 Controller
  • Controller 接收请求,调用 Model 更新数据,再通知 View 刷新
  • ModelView 之间通过 Controller 解耦

中介者模式 vs 观察者模式

这是面试高频对比题。两者都处理对象间的通信,但设计理念截然不同:

维度中介者模式观察者模式
通信方向双向(中介者协调同事之间的交互)单向(Subject 通知 Observer)
控制方式集中控制 — 中介者知道所有同事,包含交互逻辑分散通知 — Subject 只负责通知,不关心 Observer 如何响应
耦合关系同事之间完全解耦,但依赖中介者Observer 依赖 Subject
中介者/主题的角色包含业务逻辑(谁通知谁、何时通知)只做消息分发(通知所有观察者)
典型场景表单联动、聊天室、MVC ControllerDOM 事件、React setState、Vue 响应式
复杂度转移将同事间的复杂度转移到中介者复杂度分散在各 Observer 中
一句话区分
  • 中介者模式:我(中介者)知道你们所有人,我来决定谁该和谁通信
  • 观察者模式:我(Subject)不管你们是谁,我只管广播,你们自己决定怎么处理

更多关于观察者与发布订阅的详细对比,参考 观察者模式与发布订阅模式


中介者模式 vs 发布订阅模式

发布订阅模式中的事件中心(Event Channel)和中介者在结构上很相似,但职责不同:

维度中介者模式发布订阅模式
中间层职责包含业务编排逻辑仅做消息路由(按事件名分发)
是否知道参与者中介者通常持有所有同事的引用事件中心不知道发布者和订阅者是谁
耦合度同事依赖中介者接口发布者和订阅者完全解耦
灵活性交互逻辑可精细控制通信模式固定(事件驱动)
联系

在实际前端开发中,EventBus / mitt 这类事件中心既可以看作发布订阅的事件通道,也可以看作简化版的中介者。当你在事件处理中加入条件判断、路由逻辑时,它就更接近中介者模式。


避免"上帝对象"问题

中介者模式最大的风险是中介者本身变成上帝对象(God Object) — 所有业务逻辑都集中在中介者中,导致中介者类膨胀到难以维护。

上帝对象的信号
  • 中介者类超过 300 行代码
  • 中介者中出现大量 if-else 分支
  • 新增一个同事需要修改中介者的多处代码
  • 中介者承担了与"协调通信"无关的业务逻辑

解决方案:拆分中介者

split-mediator.ts
// ❌ 膨胀的单一中介者
class GodMediator {
handleUserAction(action: string) { /* 100 行 */ }
handleCartAction(action: string) { /* 80 行 */ }
handlePaymentAction(action: string) { /* 120 行 */ }
handleNotificationAction(action: string) { /* 60 行 */ }
}

// ✅ 按职责拆分为多个中介者
class UserMediator {
handleLogin() { /* ... */ }
handleLogout() { /* ... */ }
}

class CartMediator {
handleAddItem() { /* ... */ }
handleRemoveItem() { /* ... */ }
handleCheckout() { /* ... */ }
}

class PaymentMediator {
handlePay() { /* ... */ }
handleRefund() { /* ... */ }
}

// 中介者组合层(可选)
class AppMediator {
constructor(
private userMediator: UserMediator,
private cartMediator: CartMediator,
private paymentMediator: PaymentMediator
) {}
}

何时不该使用中介者模式

  • 对象之间的交互本来就简单(2-3 个对象的直接通信就够了)
  • 交互逻辑频繁变化且差异极大,中介者会变得不稳定
  • 引入中介者的成本大于它带来的解耦收益

常见面试问题

Q1: 什么是中介者模式?解决了什么问题?

答案

中介者模式通过引入一个中介者对象,将多个对象之间的网状多对多通信转化为星型一对多通信。各对象(Colleague)不再直接引用彼此,而是通过中介者间接通信。

核心解决的问题是降低对象间的耦合度

// 没有中介者:N 个对象两两通信,N*(N-1)/2 条连接
// 4 个组件 → 6 条连接
// 10 个组件 → 45 条连接

// 有中介者:N 个对象只需 N 条连接(各自只连中介者)
// 4 个组件 → 4 条连接
// 10 个组件 → 10 条连接

现实例子:机场塔台(中介者)协调所有飞机(Colleague)的起降,飞机之间不直接通信。


Q2: 前端开发中有哪些中介者模式的实际应用?

答案

应用场景中介者角色Colleague 角色
表单联动FormMediator各表单控件(省/市/区、日期/时间)
Vue EventBus / mitt事件中心各 Vue 组件
React Context + useReducerContext Provider消费 Context 的子组件
微前端通信GlobalMediator各子应用
MVC 架构ControllerModel 和 View
聊天室/IMChatRoom / Server各用户客户端
状态管理库(Redux/Vuex)Store各组件

其中,Redux 的 Store 也是典型的中介者 — 所有组件通过 dispatch 发送 Action 到 Store,Store 根据 Reducer 计算新状态并通知订阅者。


Q3: 中介者模式和观察者模式的核心区别是什么?

答案

最核心的区别是中间层是否包含业务逻辑

  • 中介者模式:中介者包含交互逻辑,它知道所有同事,决定"谁该通知谁"、"何时通知"
  • 观察者模式:Subject 不包含交互逻辑,只是简单地广播通知给所有 Observer
// 中介者 — 包含路由逻辑
class ChatRoom {
sendMessage(msg: string, sender: User, receiver?: User): void {
if (receiver) {
receiver.receive(msg, sender); // 私聊:只通知指定用户
} else {
this.users.forEach((u) => {
if (u !== sender) u.receive(msg, sender); // 群发:通知所有人
});
}
}
}

// 观察者 — 无差别广播
class Subject {
notify(data: unknown): void {
this.observers.forEach((obs) => obs.update(data)); // 不做任何判断,全部通知
}
}

Q4: EventBus(如 mitt)是中介者模式还是发布订阅模式?

答案

两者都沾,取决于使用方式:

  • 当 EventBus 仅作为消息路由器(按事件名分发,不含业务逻辑)时,更接近发布订阅模式
  • 当你在事件处理器中引入了条件判断、协调逻辑时,EventBus 就充当了中介者
import mitt from 'mitt';
const bus = mitt();

// 纯发布订阅 — bus 只做路由
bus.on('click', (data) => console.log(data));
bus.emit('click', { x: 100 });

// 偏中介者 — 在处理器中协调多个模块
bus.on('order:create', (order) => {
// 中介者逻辑:协调多个模块的响应
inventoryModule.deductStock(order.items);
paymentModule.createPayment(order.total);
notificationModule.sendConfirmation(order.userId);
});

实际开发中不必过于纠结分类,理解它们的设计意图更重要。


Q5: 如何避免中介者变成"上帝对象"?

答案

中介者膨胀是该模式最大的缺点。防治策略:

  1. 按领域拆分:将一个大中介者拆为多个小中介者(UserMediator、CartMediator、PaymentMediator)
  2. 使用策略模式:将中介者中的条件分支抽取为策略对象
  3. 结合事件驱动:用事件解耦中介者内部逻辑,减少硬编码的条件判断
  4. 设定规模阈值:当中介者超过 200-300 行代码时,主动重构
// ✅ 拆分 + 组合
class AppCoordinator {
constructor(
private auth: AuthMediator,
private cart: CartMediator,
private notification: NotificationMediator
) {}

// 只在顶层协调跨域交互
onUserLogin(userId: string): void {
this.auth.login(userId);
this.cart.loadCart(userId);
this.notification.loadPreferences(userId);
}
}

Q6: MVC 中的 Controller 为什么是中介者?

答案

在 MVC 架构中:

  • ModelView 互不知道对方的存在
  • Controller 持有 Model 和 View 的引用,负责协调交互
  • 用户操作 View → Controller 接收请求 → 调用 Model 更新数据 → 通知 View 刷新

这完全符合中介者模式的定义:Controller 就是中介者,Model 和 View 就是 Colleague。

mvc-controller-as-mediator.ts
class UserController {
constructor(
private model: UserModel,
private view: UserView
) {
// View 事件通过 Controller 协调
this.view.onSaveClick(() => {
const formData = this.view.getFormData();
this.model.save(formData); // 通知 Model
this.view.showSuccess('保存成功'); // 通知 View
});

// Model 变化通过 Controller 反馈给 View
this.model.onChange((data) => {
this.view.render(data);
});
}
}

在现代前端框架中,这个角色演变为:

  • React:组件本身(或自定义 Hook)
  • Vue:组件的 <script> 部分
  • Angular:Component 类 + Service

Q7: 在微前端中如何设计应用间通信的中介者?

答案

微前端场景下,多个子应用需要通信(用户状态同步、路由协调、数据共享等)。设计要点:

micro-frontend-mediator-design.ts
// 类型安全的全局中介者
interface MediatorEvents {
'auth:login': { userId: string; token: string };
'auth:logout': void;
'route:change': { path: string; appName: string };
'theme:change': { mode: 'light' | 'dark' };
}

class MicroFrontendMediator {
private listeners = new Map<string, Set<Function>>();
private stateSnapshot = new Map<string, unknown>(); // 状态快照,子应用可拉取最新状态

on<K extends keyof MediatorEvents>(
event: K,
handler: (data: MediatorEvents[K]) => void
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
// 返回取消订阅函数
return () => this.listeners.get(event)!.delete(handler);
}

emit<K extends keyof MediatorEvents>(event: K, data: MediatorEvents[K]): void {
this.stateSnapshot.set(event, data);
this.listeners.get(event)?.forEach((handler) => handler(data));
}

// 子应用延迟挂载时,可获取最新状态
getLatest<K extends keyof MediatorEvents>(event: K): MediatorEvents[K] | undefined {
return this.stateSnapshot.get(event) as MediatorEvents[K] | undefined;
}
}

// 主应用初始化
const mediator = new MicroFrontendMediator();
(window as any).__MICRO_MEDIATOR__ = mediator;

关键设计点:

  1. 类型安全:使用 TypeScript 泛型约束事件名和数据类型
  2. 状态快照:子应用可能延迟加载,需要能获取加载前的状态
  3. 自动清理on 返回取消函数,子应用卸载时清理监听
  4. 命名空间:事件名使用 app:action 格式,避免冲突

Q8: 什么时候该用中介者模式,什么时候不该用?

答案

适用场景

场景说明
多个对象之间存在复杂的交互关系如表单联动、聊天室
对象之间的通信逻辑经常变化只需修改中介者,不影响各对象
想复用独立对象但不想复用它们的连接关系中介者可替换
多个子系统/微应用需要协调通信全局中介者

不适用场景

场景原因
只有 2-3 个对象通信直接通信更简单,引入中介者是过度设计
交互逻辑极少变化解耦的收益不明显
中介者已经很复杂(上帝对象)说明需要拆分或换方案
性能敏感场景经过中介者的间接调用有额外开销(通常可忽略)
一个判断原则

如果引入中介者后,系统整体复杂度降低了 — 用;如果只是把复杂度从各对象搬到了中介者里,整体没变甚至更高 — 不用。


Q9: 用 TypeScript 实现一个类型安全的通用 Mediator

答案

typed-mediator.ts
// 类型安全的通用中介者
class TypedMediator<TEvents extends Record<string, unknown>> {
private handlers = new Map<keyof TEvents, Set<Function>>();

on<K extends keyof TEvents>(
event: K,
handler: (data: TEvents[K]) => void
): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);

return () => {
this.handlers.get(event)?.delete(handler);
};
}

emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
this.handlers.get(event)?.forEach((handler) => {
(handler as (data: TEvents[K]) => void)(data);
});
}

off<K extends keyof TEvents>(event: K, handler?: Function): void {
if (handler) {
this.handlers.get(event)?.delete(handler);
} else {
this.handlers.delete(event); // 移除该事件的所有监听
}
}

once<K extends keyof TEvents>(
event: K,
handler: (data: TEvents[K]) => void
): () => void {
const wrapper = (data: TEvents[K]) => {
handler(data);
this.handlers.get(event)?.delete(wrapper);
};
return this.on(event, wrapper);
}
}

// 使用
interface AppEvents {
'user:login': { userId: string; name: string };
'user:logout': undefined;
'theme:change': 'light' | 'dark';
}

const mediator = new TypedMediator<AppEvents>();

// ✅ 类型安全 — 事件名和 data 类型自动推导
mediator.on('user:login', (data) => {
console.log(data.userId); // string ✅
console.log(data.name); // string ✅
});

// ❌ 类型错误
// mediator.emit('user:login', { wrong: true }); // TypeScript 编译报错

Q10: 中介者模式和 Redux 有什么关系?

答案

Redux 的 Store 本质上是一个中介者

Redux 概念中介者模式对应
Store中介者(Mediator)
各 React 组件同事(Colleague)
dispatch(action)同事通知中介者
Reducer 逻辑中介者的协调逻辑
subscribe / useSelector同事从中介者获取更新

组件不直接修改其他组件的状态,而是通过 dispatch 通知 Store(中介者),Store 执行 Reducer(业务逻辑)计算新状态,再通知所有订阅的组件更新。这正是中介者模式的精髓:集中控制,松散耦合


相关链接