Published on

中介者模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

中介者模式(Mediator Pattern)是一种行为型设计模式,它通过引入一个中介者对象,来封装一系列对象之间的交互操作,从而使这些对象不再相互直接引用,降低了对象之间的耦合度。这使得一个对象的变化可以独立于其他对象的变化,更加灵活和可拓展。

为什么需要中介者模式?

在繁忙的机场中,有众多飞机需要起飞、降落,还要管理跑道的使用、飞行安排等多个环节。如果让每架飞机直接与其他飞机或服务设施(如跑道、机库)直接协调所有事务,将会非常混乱。每架飞机都需要知道其他所有飞机的状态、位置及计划,这不仅效率极低,还容易出错,且增加了风险。

此时,机场调度中心就充当了中介者的角色。所有飞机不直接相互通信,而是通过调度中心来管理起降、跑道使用权和航班调度等。飞机只需要发送请求(比如请求降落)给调度中心,由调度中心来决定何时何地安排降落,再通知飞机执行。

这样,每架飞机不需要关心其他飞机的状态或跑道的具体使用计划,大大降低了系统的复杂性,同时保证了操作的安全和高效。

在实际开发中,多个对象之间往往会发生复杂的交互。这些对象彼此联系紧密,相互依赖,这种情况会导致以下问题:

  • 高耦合:对象之间的依赖关系错综复杂,难以维护和扩展。
  • 变化复杂:修改一个对象时,需要检查和修改所有与其直接或间接依赖的对象。
  • 代码混乱:交互逻辑散布在多个对象中,导致代码难以理解和管理。

中介者模式可以解决这些问题,通过引入一个中介者对象,集中管理对象之间的交互,从而降低对象之间的耦合,提高系统的灵活性和可维护性。

基本概念

中介者模式的核心在于引入一个中介者对象,以解耦多个同事对象(Colleague)。主要包含以下角色:

  1. 中介者接口(Mediator):
    1. 功能:定义了一个用于通信的接口,该接口描述了中介者可以提供给同事对象的操作。
    2. 目的:提供一个基本的协议,让中介者实现,以便能够处理多个同事对象之间的交互。
  2. 具体中介者(Concrete Mediator):
    1. 功能:实现中介者接口,并协调各个同事类对象间的交互。
    2. 目的:具体中介者知道所有的具体同事类,并负责具体的协调工作。
  3. 同事类接口(Colleague):
    1. 功能:定义同事对象的接口,其中同事对象可以设置和提取自己的中介者对象。
    2. 目的:使得每个具体同事类都只需知道自己的中介者对象,而不是直接与其他同事类通信。
  4. 具体同事类(Concrete Colleague):
    1. 功能:实现同事类接口,知悉其相关联的中介者对象。
    2. 目的:在需要与其他同事对象交互时,具体同事类会通过中介者来间接完成。

实现示例

假设我们需要实现一个聊天系统,每个用户可以发送消息给其他用户,并通过中介者管理消息的分发。

定义中介者接口

// 中介者接口,定义同事对象之间的交互
interface ChatMediator {
  sendMessage(message: string, user: User): void;
  addUser(user: User): void;
}

定义同事类接口

// 同事类接口,定义用户对象
abstract class User {
  protected mediator: ChatMediator;
  protected name: string;

  constructor(mediator: ChatMediator, name: string) {
    this.mediator = mediator;
    this.name = name;
  }

  abstract send(message: string): void;
  abstract receive(message: string): void;
}

实现具体中介者

// 具体中介者类,实现消息分发功能
class ChatRoom implements ChatMediator {
  private users: User[] = [];

  addUser(user: User): void {
    this.users.push(user);
  }

  sendMessage(message: string, user: User): void {
    for (const u of this.users) {
      if (u !== user) {
        u.receive(message);
      }
    }
  }
}

实现具体同事类

// 具体同事类,实现消息发送和接收
class ConcreteUser extends User {
  constructor(mediator: ChatMediator, name: string) {
    super(mediator, name);
  }

  send(message: string): void {
    console.log(`${this.name} sends: ${message}`);
    this.mediator.sendMessage(message, this);
  }

  receive(message: string): void {
    console.log(`${this.name} receives: ${message}`);
  }
}

使用中介者模式实现聊天功能

// 创建中介者
const chatMediator = new ChatRoom();

// 创建用户
const user1 = new ConcreteUser(chatMediator, 'Alice');
const user2 = new ConcreteUser(chatMediator, 'Bob');
const user3 = new ConcreteUser(chatMediator, 'Charlie');

// 添加用户到聊天室
chatMediator.addUser(user1);
chatMediator.addUser(user2);
chatMediator.addUser(user3);

// 用户发送消息
user1.send("Hi everyone!");
user2.send("Hello Alice!");
Alice sends: Hi everyone!
Bob receives: Hi everyone!
Charlie receives: Hi everyone!
Bob sends: Hello Alice!
Alice receives: Hello Alice!
Charlie receives: Hello Alice!

应用场景

组件通信

对于大型的 Web 应用来说,组件间的通信是一个非常常见的问题。当应用复杂时,组件间直接通信的方式会变得难以管理。在这种情况下,可以引入一个中介者(如 Vuex、Redux 等状态管理库),以统一管理组件间的状态和消息传递,组件不直接相互引用,而是通过中介者进行交互。

示例:使用 Vuex 实现组件间通信

在这个示例中,我们通过 Vuex 管理组件间的状态和消息传递。Vuex 充当了中介者的角色,统一管理状态。

// Vuex store
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  }
});

// Vue component
new Vue({
  computed: {
    count() {
      return store.state.count;
    }
  },
  methods: {
    increment() {
      store.commit('increment');
    }
  }
});

通过 Vuex 统一管理状态,实现组件间的消息传递,使得组件间不直接相互引用,降低了耦合度。

插件系统

在拥有众多插件的应用中,插件与核心系统或其他插件之间的通信可以通过一个中介者对象来协调,这样插件只需要通过中介者交互,而不是直接与其他插件通信,减少了插件之间的依赖。

示例:简单的插件系统

// 定义 Plugin 类
class Plugin {
  private mediator: PluginMediator;

  constructor(mediator: PluginMediator) {
    this.mediator = mediator;
  }

  performAction() {
    console.log('Plugin action performed');
  }

  notifyMediator() {
    this.mediator.notify(this, 'performAction');
  }
}

// 定义中介者接口
interface PluginMediator {
  notify(sender: Plugin, event: string): void;
}

// 定义具体中介者
class ConcretePluginMediator implements PluginMediator {
  private plugins: Plugin[] = [];

  addPlugin(plugin: Plugin) {
    this.plugins.push(plugin);
  }

  notify(sender: Plugin, event: string): void {
    console.log(`Mediator received event '${event}' from plugin`);
    this.plugins.forEach(plugin => {
      if (plugin !== sender) {
        plugin.performAction();
      }
    });
  }
}

// 使用插件系统
const mediator = new ConcretePluginMediator();
const plugin1 = new Plugin(mediator);
const plugin2 = new Plugin(mediator);

mediator.addPlugin(plugin1);
mediator.addPlugin(plugin2);

plugin1.notifyMediator(); // 插件1 触发事件,中介者通知其他插件执行操作

通过中介者系统,插件只需要与中介者交互,不需要直接与其他插件通信,从而简化了插件之间的依赖,增强了系统的可扩展性。

页面事件处理

在复杂页面上,可能有多个模块需要对同一个事件做出响应。例如,滚动事件可能需要被用于回到顶部的按钮、懒加载图片、动态加载数据等模块。通过中介者模式,可以将事件监听和响应的逻辑统一到一个中介者中,其他模块只需要与中介者通信即可。

示例:页面滚动事件处理

// 定义中介者
class ScrollMediator {
  private listeners: { [event: string]: Function[] } = {};

  subscribe(event: string, listener: Function) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(listener);
  }

  notify(event: string, data?: any) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(listener => listener(data));
    }
  }
}

// 使用中介者
const scrollMediator = new ScrollMediator();

// 模块1:回到顶部按钮
class BackToTopButton {
  constructor(mediator: ScrollMediator) {
    mediator.subscribe('scroll', this.checkVisibility.bind(this));
  }

  checkVisibility(scrollY: number) {
    console.log('BackToTopButton checks visibility:', scrollY);
    // 逻辑: 根据 scrollY 显示或隐藏按钮
  }
}

// 模块2:懒加载图片
class LazyLoadImages {
  constructor(mediator: ScrollMediator) {
    mediator.subscribe('scroll', this.loadImages.bind(this));
  }

  loadImages(scrollY: number) {
    console.log('LazyLoadImages loads images:', scrollY);
    // 逻辑: 根据 scrollY 懒加载图片
  }
}

// 初始化模块
new BackToTopButton(scrollMediator);
new LazyLoadImages(scrollMediator);

// 监听滚动事件
window.addEventListener('scroll', () => {
  scrollMediator.notify('scroll', window.scrollY);
});

通过中介者模式统一管理页面滚动事件的监听和响应逻辑,减少了模块之间的相互依赖,提高了代码的可维护性。

与 RxJS 区别

页面滚动事件处理这个示例实现上和 RxJS 的运作方式有一些相似之处,但它们本质上还是有一些区别的。下面是使用 RxJS 实现上述场景的代码示例:

import { fromEvent } from 'rxjs';
import { throttleTime, map } from 'rxjs/operators';

const scroll$ = fromEvent(window, 'scroll').pipe(
  throttleTime(200),
  map(() => window.scrollY)
);

// 模块1:回到顶部按钮
const backToTopButton$ = scroll$.subscribe(scrollY => {
  console.log('BackToTopButton checks visibility:', scrollY);
  // 逻辑: 根据 scrollY 显示或隐藏按钮
});

// 模块2:懒加载图片
const lazyLoadImages$ = scroll$.subscribe(scrollY => {
  console.log('LazyLoadImages loads images:', scrollY);
  // 逻辑: 根据 scrollY 懒加载图片
});

对比下来,他们之间的区别和特点如下:

  1. 编程模型:
    1. 中介者模式:通过引入一个中介者对象来集中管理对象之间的交互,使系统结构更清晰,降低对象之间的耦合。
    2. RxJS:基于事件流和异步数据流的编程模型,通过 Observable、操作符和订阅进行声明式事件处理,适合处理复杂的异步逻辑,更贴近于观察者模式。
  2. 简化事件处理:
    1. 中介者模式:事件处理逻辑集中在中介者对象中,简单清晰,适用于需要集中管理多对象交互的场景。
    2. RxJS:通过链式操作符和流式处理,可以简化异步事件处理和组合,适用于处理复杂异步数据流的场景。
  3. 代码可读性和可维护性:
    1. 中介者模式:通过中介者解耦模块间的依赖,提升代码可读性和可维护性,适用于管理多个模块间的交互。
    2. RxJS:通过声明式操作符处理和组合流,使代码简洁、可读性高,且易于调试和测试,适用于复杂异步逻辑和事件流处理。

开发中如何选择呢?

  • 中介者模式:适合用于需要解耦多个对象复杂交互的场景。通过中介者集中管理对象之间的通信,使得系统结构更加清晰和易于管理,如在模块通信、插件系统、事件处理场景中。
  • RxJS:适合处理复杂异步数据流和事件流的场景。通过声明式链式操作符,简化异步事件处理和组合逻辑,提高代码可读性和可维护性,如在实时数据流处理、复杂表单处理等场景中。

典型案例

Event Bus/Event Emitter

事件总线或事件发射器也是一种中介者模式的体现。它们提供了一个中心点,允许不同模块或组件在不知道彼此直接存在的情况下进行交流。例如,Vue 实例可以作为一个事件中心,各个组件可以通过它来广播和监听事件。

// Vue.js Event Bus 示例
const EventBus = new Vue();

Vue.component('component-a', {
  created() {
    EventBus.$on('event', this.handleEvent);
  },
  methods: {
    handleEvent(message: string) {
      console.log('Component A received:', message);
    }
  },
  template: '<div>Component A</div>'
});

Vue.component('component-b', {
  methods: {
    emitEvent() {
      EventBus.$emit('event', 'Hello from Component B!');
    }
  },
  template: '<div><button @click="emitEvent">Send</button></div>'
});

new Vue({ el: '#app' });

WebSocket 通信管理

在使用 WebSocket 进行实时数据通信的应用中,通常会有多处需要处理数据或发送数据的场景。通过实现一个 WebSocket 的管理模块作为中介者,可以统一处理连接、断开、发送消息等逻辑,各个模块只需与这个中介者交互即可,降低了系统的复杂度。

// WebSocket 管理模块
class WebSocketManager {
  private socket: WebSocket;
  private listeners: { [event: string]: Function[] } = {};

  constructor(url: string) {
    this.socket = new WebSocket(url);
    this.socket.onmessage = (event) => this.notify('message', event.data);
  }

  subscribe(event: string, listener: Function) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(listener);
  }

  notify(event: string, data?: any) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(listener => listener(data));
    }
  }

  sendMessage(message: string) {
    this.socket.send(message);
  }
}

// 使用 WebSocketManager
const wsManager = new WebSocketManager('ws://example.com/socket');

// 模块1:实时数据更新
class LiveDataUpdater {
  constructor(manager: WebSocketManager) {
    manager.subscribe('message', this.updateData.bind(this));
  }

  updateData(data: string) {
    console.log('LiveDataUpdater received:', data);
    // 逻辑: 更新实时数据
  }
}

// 初始化模块
new LiveDataUpdater(wsManager);

// 模拟发送消息
setTimeout(() => {
  wsManager.sendMessage('Hello WebSocket!');
}, 1000);

优缺点

优点

  1. 降低耦合:中介者模式将对象之间的直接交互解耦,通过中介者进行通信,降低了对象之间的依赖关系。
  2. 集中控制:将复杂的交互逻辑集中到中介者中,使系统结构清晰,易于理解和维护。
  3. 灵活性:通过引入中介者对象,能够更灵活地改变对象之间的交互规则和顺序。

缺点

  1. 可能导致中介者过于复杂:如果对象之间的交互过于复杂,会导致中介者类变得非常庞大和复杂,难以维护和扩展。

总结

中介者模式是一种行为设计模式,它通过引入一个中介者角色来简化多个对象或类之间的通信。该模式的核心思想是通过一个中介对象来封装一系列的对象交互,使得对象之间不需要显式地相互引用,从而达到降低系统的复杂度,提高可维护性和可扩展性。

在软件开发中,尤其是在前端框架和应用中,中介者模式被广泛应用于处理组件或模块间的通信问题,如在Vue或React等单页应用(SPA)中通过Vuex、Redux等状态管理库管理应用状态,在组件间共享和通信,或通过Event Bus/Emitter实现跨组件的事件通信,以及在实时通信应用中管理WebSocket连接等。通过中介者模式,开发者能够编写出更加清晰、结构化和易于管理的代码,有效提升开发效率和应用性能。