原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#mediatorpatternjavascript

在观察者模式的章节中,我们了解了一种通单个对象来与多个事件源通信的方式。它也被称为发布/订阅或者事件聚合。遇到这个问题时,开发者通常会想到中介者模式,那么就让我们来探讨一下它们的区别。

中介者在字典的含义是一个用于协助谈判和解决冲突的中立方。在程序员的世界中,中介者是一个行为设计模式,它允许我们暴露出一个统一的接口,系统的不同部分可以通过它相互通信。

如果一个系统中的组件间出现太多的直接关联,就可能是时候建立一个集中控制点来替代取代组件间原来的通信方式。中介者通过确保是通过中心点来处理组件间通信,而不是在组件中明确地相互引用,以此来减少耦合。这有助于我们解耦系统,来提升组件复用的潜力。

中介者在真实世界中可以类比成一个典型的航空交通控制系统。塔台(中介者)处理哪些飞机可以起飞或降落,这是因为所有的通信(正在监听的通知或广播)都是在飞机和控制台之间完成的,而不是飞机之间直接完成的。中央控制器是这个系统成功的关键,这也是中介者在软件设计中扮演的角色。

另一个类比就是 DOM 事件冒泡和事件委托。如果系统中所有的订阅都在 document 上而不是其他单个的节点,那么 doument 实际上起到的就是中介者的作用。一个更高层级的对象将负责通知订阅者关于互动事件,而不是将事件绑定在单个的节点上。

谈到中介者模式和事件聚合模式(Event Aggregator patterns),由于实现上有些相似点,他们看起来似乎是可以相互替换的。然而,它们在语法和意图上却非常不同。

即使是它们实现都使用相同的核心结构,我相信它们之间是有明显的不同。我还认为由于这个区别的存在,在沟通交流中它们不应该被互换或混淆。

一个简单的中介者

中介者是一个协调多个对象之间交互(逻辑和行为)的对象。它会根据其他对象的动作(或者交互)和输入来决定什么时候去调用哪个对象。

你可以使用下面这一行代码来声明一个中介者。

  1. var mediator = {};

是的,这的确就只是 JavaScript 中的一个对象字面量。再强调一次,这里我们从语义的角度来讨论。中介者的目的是控制对象间的工作流,有了对象字面量的话我们也不再需要其他东西来做这个事情。

  1. var orgChart = {
  2. addNewEmployee: function(){
  3. // getEmployeeDetail 提供了一个用户与之交互的视图
  4. var employeeDetail = this.getEmployeeDetail();
  5. // 当员工详情完成时,中介者(orgChart 对象)决定接下来要做什么
  6. employeeDetail.on("complete", function(employee){
  7. // 新建包含更多事件的对象,它将被用中介者用来做更多的事情
  8. var managerSelector = this.selectManager(employee);
  9. managerSelector.on("save", function(employee){
  10. employee.save();
  11. });
  12. });
  13. },
  14. // ...
  15. }

这个例子展示了一个非常基础的中介者的实现,它带有一些用于来触发和订阅事件的实用方法。

过去我经常把这类对象称为“工作流程”对象,但事实上它是一个中介者。它是一个处理很多其他对象间的工作流的对象,它将工作流知识的职责聚集到一个对象中。这样可以使工作流更容易理解和维护。

相似与区别

毫无疑问,从我上面展示的例子中就能看得出事件聚合模式和中介者模式是有相同点的。相似处可以总结为两点:事件和第三方对象。不过这些差异只是表面的。当我们深入了解模式的意图,会看到这些模式的实现有明显的差异,模式的本质将变得更加明显。

事件

在上面的例子中,事件聚合模式和中介者模式都使用事件。事件聚合模式显然是用事件来处理的,毕竟名字上都有体现了。中介者模式使用事件只是因为它能使与现代 webapp 框架协作时更加轻松。中介者没有被要求一定要用事件来实现。你也可以通过回调函数来创建一个中介者,通过将中介者引用传递给子对象,或者其他方法中任意一种来创建中介。

另一个区别是这两种模式为什么使用事件。事件聚合器作为一种模式,被设计用来处理事件。然而中介者使用事件仅仅是因为它比较方便。

第三方对象

在设计上,事件聚合器和中介者都是使用一个第三方对象简化事情。事件聚合器是相对于事件发布者和事件订阅者的第三方。它充当事件传递的中心枢纽。可是中介者也是其他对象的第三方。那么区别在哪里呢?我们为什么不把事件聚合器称为一个中介呢?答案很大程度上取决于应用程序逻辑和工作流程的编码位置。

在事件聚合器的情况中,第三方对象只是为了便于事件从未知数量的源传递到未知数量的处理程序。所有需要启动的工作流和业务逻辑都直接放入到触发事件的对象和处理事件的对象中。

然而,在中介者的情况中,业务逻辑和工作流都全部是置于中介者本身。中介者根据它所知的情况来决定对象的方法何时被调用和属性何时更新。它封装了工作流和流程,协调多个对象以产生所需的系统行为。这个工作流中的各个对象都知道如何执行自己的任务。但是是中介者通过比这些独立对象更高的层次上来做出决策来告诉对象何时执行任务。

事件聚合器促成了一种“射后不管”的通信模型。触发事件的对象并不关心是否有对应的订阅者。它只是触发事件,就继续其他的任务。然而中介者可能会用事件来做决定,但绝对不是“射后不管”。中介者会关注一组已知的输入或活动,以便它可以促进和协调与一组已知的参与者(对象)的附加行为。

关系:何时用哪个

出于语义的原因,了解事件聚合器和中介者的异同非常重要。然而了解何时使用哪种模式也十分重要。模式的基本语义和意图确实会告诉我们何时来使用它,但是使用模式的实际经验会帮助你理解更细微的要点和要作出的细微的决定。

使用事件聚合器

通常,事件聚合器是在当你既有很多对象需要直接监听,这些对象又是完全不相关时使用。
当两个对象已经有直接的关联,也就是说父视图和子视图,使用事件聚合器更好。让子视图来触发事件,父视图可以处理事件。在 JavaScript 框架术语中,它常见于 Backbone 的 Collection 和 Model,其中所有的 Model 事件都被冒泡到并通过父 Collection。Collection 通常使用 model 的事件来修改它自己或者其他 model 的状态。处理 collection 中的 “选中” 项就是一个好例子。

作为事件聚合器,jQuery 的 on 方法是一个关于有太多对象需要监听的好例子。如果你有10、20甚至是200个 DOM 元素可以触发 “click” 事件,单独为所有这些元素设置监听器可能是坏主意。这可能会迅速降低应用的性能和用户体验。相反,jQuery 的 on 方法允许我们聚合所有的事件,并把10、20或者200个事件处理程序开销降低到1个。

间接关系也是使用事件聚合器的好时机。在现代应用中,经常会有需要相互通信但又没有直接关联的视图对象。例如,菜单系统可能会有一个处理菜单项点击的视图。但是我们不希望菜单直接和内容视图直接关联,内容视图是当菜单项被点击时展示的详情和信息。从长远的角度来看,如果将内容和菜单耦合在一起会让代码维护变得很困难。相反,我们可以用事件聚合器来触发 menu:click:foo 事件,并且通过一个 foo 对象处理点击事件,并将其内容展示在屏幕上。

使用中介者

当两个或多个对象有间接的工作关系,并且业务逻辑或者工作流需要规定这些对象的交互和协作时,最好使用中介者。向导界面就是一个好例子,如上面展示的 “orgChart” 实例。有多个视图可以促进向导的整个工作流。我们可以通过引入中介者来解耦视图,并明确地对它们的工作流建模,而不是让它们相互直接引用而紧紧的耦合在一起。

中介者从实现细节中抽离出工作流,并且在更高的层次上创建更自然的抽象,让我们更快的了解工作流是什么样子。我们不再需要深入到工作流的每一个视图详情中去了解工作流实际的情况。

事件聚合器(发布/订阅)和中介者一起使用

区分事件聚合和中介者的关键点以及为何它们的命名不能互换,最好是通过将它们一起使用来说明。事件聚合器中的菜单示例也同样可以引入中介者。

点击菜单项可能会触发遍布整个应用的一系列变更。其中一些变更是独立于其他变更,使用事件聚合器就很合适。然而有些变更在内部是相互关联的,可以使用中介者来实施这些改动。

然后,中介者可以用来监听事件聚合器。它可以执行其的逻辑和过程,来促进和协调许多彼此关联但又和源事件无关的许多对象。

  1. var MenuItem = MyFrameworkView.extend({
  2. events: {
  3. "click .thatThing": "clickedIt"
  4. },
  5. clickedIt: function(e){
  6. e.preventDefault();
  7. // 假设这里触发了 "menu:click:foo"
  8. MyFramework.trigger("menu:click:" + this.model.get("name"));
  9. }
  10. });
  11. // ... 应用中的其他地方
  12. var MyWorkflow = function(){
  13. MyFramework.on("menu:click:foo", this.doStuff, this);
  14. };
  15. MyWorkflow.prototype.doStuff = function(){
  16. // 在这里实例化多个对象。
  17. // 为这些对象设置事件处理程序。
  18. // 将所有对象协同成一个有意义的工作流。
  19. };

在这个例子中,当具有正确模型的 MenuItem 被点击时, menu:click:foo 事件就会被触发。假设有一个已经实例化了的“MyWorkflow” 的实例对象,它将会处理这个特定的事件,并且会协调它所知道的所有对象,来创造出期望的用户体验的工作流。

事件聚合器和中介者一起用来在代码和应用程序本身创建了更有意义的体验。现在我们通过事件聚合器在菜单和工作流有了一个清晰的界限,并且我们通过中介者仍然保持了工作流本身清晰和可维护。

优点和缺点

中介者模式最大的有点就是它将系统中的对象或组件间的通信通道从多对多减少到多对一。由于现有的去耦级别,增加新的发布者和订阅者变得相对容易。

或许使用这个模式最大坏处就是它引入了单点故障。在模块间放置中介者还会导致性能下降,因为它们总是间接通信。因为松耦合的性质,我们就很难仅仅通过广播来确定系统的反应方式。

也就是说,提醒我们自己解耦的系统有一些其他的好处 - 如果我们的模块直接的同其他模块通信,模块的改动(例如,其他模块抛出一个异常)可能会很容易对我们系统的其他部分产生多尼诺骨牌效应。在解耦的系统中则不太需要关心这个问题。

最终,紧耦合会导致各种头痛并且它只是另一种替代解决方案,但如果实施正确,这种方案会很有效。

中介者 vs 外观模式

我们很快就要讲到外观模式,处于参考的目的,一些开发者可能还想知道中介者模式和外观模式之间是否有相似点。它们都抽象了现有模块的功能,但是也有一些细微的差异。

中介者集中化了模块间的相互明确引用的通信部分。某种意义上,这是多向的。然而外观模式只是定义了模块或系统简单的接口,并没有增加额外的功能。系统中的其他模块没有直接意识到外观模式的概念,可以认为是单向的。