中介者模式
问题
什么是中介者模式?它解决了什么问题?前端开发中有哪些典型的中介者模式应用?中介者模式和观察者模式、发布订阅模式有什么区别?
答案
中介者模式(Mediator Pattern)是 GoF 23 种经典设计模式之一,属于行为型模式。它通过引入一个中介者对象来封装一组对象之间的交互,使得各对象之间不再直接引用,从而实现松耦合。
GoF 定义:定义一个对象来封装一组对象之间的交互。中介者通过使对象之间不需要显式地相互引用,从而使其耦合松散,并且可以独立地改变它们之间的交互。
核心概念与角色
中介者模式包含两个核心角色:
| 角色 | 说明 |
|---|---|
| Mediator(中介者) | 定义与各 Colleague 通信的接口,协调各 Colleague 之间的交互逻辑 |
| Colleague(同事/参与者) | 每个参与者只知道中介者,不直接与其他参与者通信 |
中介者模式的本质是将网状的多对多通信转化为星型的一对多通信,所有交互逻辑都集中到中介者中管理。
网状拓扑 vs 星型拓扑
没有中介者时,N 个组件之间的通信是网状的,每个组件都需要知道其他组件的存在:
引入中介者后,通信变为星型拓扑,所有组件只与中介者通信:
- 无中介者:N 个组件需要 条连接(如 4 个组件需 6 条连接,10 个组件需 45 条连接)
- 有中介者:N 个组件只需要 条连接(每个组件只连接中介者)
TypeScript 实现 — 聊天室示例
聊天室是中介者模式最经典的例子:ChatRoom 作为中介者,User 作为 Colleague。
// ========== 中介者接口 ==========
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 来协调所有联动逻辑:
// 表单控件接口
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)本质上就是一个中介者:
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 推荐使用 mitt、Pinia 或 provide/inject。更多内容参考 Vue 组件通信方式。
3. React Context 作为中介者
React 的 Context 可以充当中介者角色,在组件树中共享状态和方法,子组件通过 Context 间接通信:
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
在微前端架构中,多个子应用之间需要通信(如登录状态同步、路由协调等),使用全局中介者避免子应用之间的直接依赖:
// 全局中介者
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 刷新
- Model 和 View 之间通过 Controller 解耦
中介者模式 vs 观察者模式
这是面试高频对比题。两者都处理对象间的通信,但设计理念截然不同:
| 维度 | 中介者模式 | 观察者模式 |
|---|---|---|
| 通信方向 | 双向(中介者协调同事之间的交互) | 单向(Subject 通知 Observer) |
| 控制方式 | 集中控制 — 中介者知道所有同事,包含交互逻辑 | 分散通知 — Subject 只负责通知,不关心 Observer 如何响应 |
| 耦合关系 | 同事之间完全解耦,但依赖中介者 | Observer 依赖 Subject |
| 中介者/主题的角色 | 包含业务逻辑(谁通知谁、何时通知) | 只做消息分发(通知所有观察者) |
| 典型场景 | 表单联动、聊天室、MVC Controller | DOM 事件、React setState、Vue 响应式 |
| 复杂度转移 | 将同事间的复杂度转移到中介者 | 复杂度分散在各 Observer 中 |
- 中介者模式:我(中介者)知道你们所有人,我来决定谁该和谁通信
- 观察者模式:我(Subject)不管你们是谁,我只管广播,你们自己决定怎么处理
更多关于观察者与发布订阅的详细对比,参考 观察者模式与发布订阅模式。
中介者模式 vs 发布订阅模式
发布订阅模式中的事件中心(Event Channel)和中介者在结构上很相似,但职责不同:
| 维度 | 中介者模式 | 发布订阅模式 |
|---|---|---|
| 中间层职责 | 包含业务编排逻辑 | 仅做消息路由(按事件名分发) |
| 是否知道参与者 | 中介者通常持有所有同事的引用 | 事件中心不知道发布者和订阅者是谁 |
| 耦合度 | 同事依赖中介者接口 | 发布者和订阅者完全解耦 |
| 灵活性 | 交互逻辑可精细控制 | 通信模式固定(事件驱动) |
在实际前端开发中,EventBus / mitt 这类事件中心既可以看作发布订阅的事件通道,也可以看作简化版的中介者。当你在事件处理中加入条件判断、路由逻辑时,它就更接近中介者模式。
避免"上帝对象"问题
中介者模式最大的风险是中介者本身变成上帝对象(God Object) — 所有业务逻辑都集中在中介者中,导致中介者类膨胀到难以维护。
- 中介者类超过 300 行代码
- 中介者中出现大量
if-else分支 - 新增一个同事需要修改中介者的多处代码
- 中介者承担了与"协调通信"无关的业务逻辑
解决方案:拆分中介者
// ❌ 膨胀的单一中介者
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 + useReducer | Context Provider | 消费 Context 的子组件 |
| 微前端通信 | GlobalMediator | 各子应用 |
| MVC 架构 | Controller | Model 和 View |
| 聊天室/IM | ChatRoom / 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: 如何避免中介者变成"上帝对象"?
答案:
中介者膨胀是该模式最大的缺点。防治策略:
- 按领域拆分:将一个大中介者拆为多个小中介者(UserMediator、CartMediator、PaymentMediator)
- 使用策略模式:将中介者中的条件分支抽取为策略对象
- 结合事件驱动:用事件解耦中介者内部逻辑,减少硬编码的条件判断
- 设定规模阈值:当中介者超过 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 架构中:
- Model 和 View 互不知道对方的存在
- Controller 持有 Model 和 View 的引用,负责协调交互
- 用户操作 View → Controller 接收请求 → 调用 Model 更新数据 → 通知 View 刷新
这完全符合中介者模式的定义:Controller 就是中介者,Model 和 View 就是 Colleague。
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: 在微前端中如何设计应用间通信的中介者?
答案:
微前端场景下,多个子应用需要通信(用户状态同步、路由协调、数据共享等)。设计要点:
// 类型安全的全局中介者
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;
关键设计点:
- 类型安全:使用 TypeScript 泛型约束事件名和数据类型
- 状态快照:子应用可能延迟加载,需要能获取加载前的状态
- 自动清理:
on返回取消函数,子应用卸载时清理监听 - 命名空间:事件名使用
app:action格式,避免冲突
Q8: 什么时候该用中介者模式,什么时候不该用?
答案:
适用场景:
| 场景 | 说明 |
|---|---|
| 多个对象之间存在复杂的交互关系 | 如表单联动、聊天室 |
| 对象之间的通信逻辑经常变化 | 只需修改中介者,不影响各对象 |
| 想复用独立对象但不想复用它们的连接关系 | 中介者可替换 |
| 多个子系统/微应用需要协调通信 | 全局中介者 |
不适用场景:
| 场景 | 原因 |
|---|---|
| 只有 2-3 个对象通信 | 直接通信更简单,引入中介者是过度设计 |
| 交互逻辑极少变化 | 解耦的收益不明显 |
| 中介者已经很复杂(上帝对象) | 说明需要拆分或换方案 |
| 性能敏感场景 | 经过中介者的间接调用有额外开销(通常可忽略) |
如果引入中介者后,系统整体复杂度降低了 — 用;如果只是把复杂度从各对象搬到了中介者里,整体没变甚至更高 — 不用。
Q9: 用 TypeScript 实现一个类型安全的通用 Mediator
答案:
// 类型安全的通用中介者
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(业务逻辑)计算新状态,再通知所有订阅的组件更新。这正是中介者模式的精髓:集中控制,松散耦合。