《JavaScript设计模式》——9.6 Mediator(中介者)模式

简介:

本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.6节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。

9.6 Mediator(中介者)模式

在字典里,中介者是指“协助谈判和解决冲突的中立方”1。在本书设计模式里,中介者是一种行为设计模式,它允许我们公开一个统一的接口,系统的不同部分可以通过该接口进行通信。

如果一个系统的各个组件之间看起来有太多的直接关系,也许是时候需要一个中心控制点了,以便各个组件可以通过这个中心控制点进行通信。Mediator模式促进松散耦合的方式是:确保组件的交互是通过这个中心点来处理的,而不是通过显式地引用彼此。这种模式可以帮助我们解耦系统并提高组件的可重用性。

现实世界的一个例子是典型的机场交通控制系统。机场控制塔(中介者)处理飞机的起飞和降落,因为所有通信(监听到或发出的通知)都是从飞机到控制塔,而不是飞机和飞机直接相互通信的。中央控制系统是该系统成功的关键,而这才是中介者在软件设计中所担任的真正角色(图9-5)。
screenshot

就实现而言,Mediator模式本质上是Observer模式的共享目标。它假设该系统中对象或模块之间的订阅和发布关系被牺牲掉了,从而维护中心联络点。

它也可能被认为是额外的或者是用于应用程序间的通知,如不同子系统之间的通信,这些子系统本身就很复杂,且可能希望通过发布/订阅关系实现内部组件间的解耦。

另一个例子是 DOM 事件冒泡和事件委托。如果系统中所有的订阅针对的是文档document而不是单个node节点,则这个文档会有效地充当中介者。更高级别(level)的对象承担了向订阅者通知有关交互事件的责任,而不是绑定到单个节点的事件。
**
9.6.1 基本实现**
可以在下面找到Mediator模式的简单实现,暴露了publish()和subscribe()方法来使用:

var mediator = (function (){
// 存储可被广播或监听的topic
var topics = {};
// 订阅一个topic,提供一个回调函数,一旦topic被广播就执行该回调
var subscribe = function (topic, fn){
     if (!topics[topic]){
       topics[topic] = [];
     }
     topics[topic].push({ context: this, callback: fn });
     returnthis;
};
// 发布/广播事件到程序的剩余部分
var publish = function (topic){
     var args;
     if (!topics[topic]){
       return false;
     }
     args = Array.prototype.slice.call(arguments, 1);
     for (var i = 0, l = topics[topic].length; i < l; i++) {
              var subscription = topics[topic][i];
          subscription.callback.apply(subscription.context, args);
     }
     return this;
};
return {
     Publish: publish,
     Subscribe: subscribe,
     installTo: function (obj) {
          obj.subscribe = subscribe;
          obj.publish = publish;
     }
};
})();

9.6.2 高级实现
如果你对更高级的代码实现感兴趣,深入下去可以浏览到 Jack Lawson 的优秀Mediator.js(//thejacklawson.com/Mediator.js/)的简洁版。除了其他改进以外,这个版本支持 topic 命名空间、订阅者删除和用于中介者的更强大的发布/订阅(Publish/Subscribe)系统。但如果你想跳过这些内容,则可以直接进入下一个示例继续阅读。2

首先,让我们来实现订阅者的概念,可以考虑一个Mediator的topic注册实例。

通过生成对象实例,之后我们可以很容易地更新订阅者,而不需要注销并重新注册它们。订阅者可以写成构造函数,该函数接受三个参数:一个可被调用的函数fn、一个options对象和一个context(上下文)。

// 将context上下文传递给订阅者,默认上下文是window对象
(function (root){
  function guidGenerator() { /*..*/ }
  // 订阅者构造函数
  function Subscriber(fn, options, context) {
     if (!this instanceof Subscriber) {
      retur nnew Subscriber(fn, context, options);
    } else {
      // guidGenerator()是一个函数,用于为订阅者生成GUID,以便之后很方便地引用它们。
      // 为了简洁,跳过具体实现
      this.id = guidGenerator();
      this.fn = fn;
      this.options = options;
      this.context = context;
      this.topic = null;
    }
  }
})();

Mediator中的topic持有了一组回调函数和子topic列表,一旦Mediator.Publish方法在Mediator实例上被调用时,这些回调函数就会被触发。它还包含用于操作数据列表的方法。

// 模拟Topic
// JavaScript允许我们使用Function对象作为原型的结合与新对象和构造函数一起调用

function Topic( namespace ){
  if ( !this instanceof Topic ) {
     return new Topic( namespace );
  }else{
    this.namespace = namespace || "";
    this._callbacks = [];
    this._topics = [];
    this.stopped = false;
  }
}

// 定义topic的prototype原型,包括添加订阅者和获取订阅者的方式

Topic.prototype = {
  // 添加新订阅者
  AddSubscriber: function( fn, options, context ){
    var callback = new Subscriber( fn, options, context );
    this._callbacks.push( callback );
    callback.topic = this;
    return callback;
    },

我们的Topic实例作为一个参数传递给Mediator回调。然后可以StopPropagation()的简便方法来调用进一步的回调传播:

StopPropagation: function(){
  this.stopped = true;
},

当给定GUID标识符时,我们也可以很容易获取现有的订阅者:

GetSubscriber: function( identifier ){
  for(var x = 0, y = this._callbacks.length; x < y; x++ ){
    if( this._callbacks[x].id == identifier || this._callbacks[x].fn == identifier ){
      return this._callbacks[x];
    }
}
for( var z in this._topics ){
  if( this._topics.hasOwnProperty( z ) ){
    var sub = this._topics[z].GetSubscriber( identifier );
    if( sub !== undefined ){
       return sub;
    }
  }
}
  },

接下来,如果需要它们,我们可以提供简单方法来添加新topic、检查现有topic或者获取topic:

AddTopic: function( topic ){
   this._topics[topic] = new Topic( (this.namespace ? this.namespace + ":" : "") + topic );
},
HasTopic: function( topic ){
   return this._topics.hasOwnProperty( topic );
},
     returnTopic: function( topic ){
     Return  this._topics[topic];
},

如果不再需要订阅者,我们可以显式地删除它们。以下代码将通过它的子主题递归删除一位订阅者:

RemoveSubscriber: function( identifier ){
   if( !identifier ){
     this._callbacks = [];
     for( var z in this._topics ){
       if( this._topics.hasOwnProperty(z) ){
          this._topics[z].RemoveSubscriber( identifier );
       }
     }
  }
for( var y = 0, x = this._callbacks.length; y < x; y++ ) {
   if( this._callbacks[y].fn == identifier || this._callbacks[y].id == identifier ){
     this._callbacks[y].topic = null;
     this._callbacks.splice( y,1 );
     x--; y--;
  }
 }
},

接下来,我们将通过子topic递归向订阅者发布(Publish)任意参数。

Publish: function( data ){
   for( var y = 0, x = this._callbacks.length; y < x; y++ ) {
      var callback = this._callbacks[y], l;
         callback.fn.apply( callback.context, data );
     l = this._callbacks.length;
     if( l < x ){
       y--;
       x = l;
     }
}
for( var x in this._topics ){
  if( !this.stopped ){
     if( this._topics.hasOwnProperty( x ) ){
       this._topics[x].Publish( data );
     }
  }
}
this.stopped = false;
}
};

这里暴露了我们将主要与之交互的Mediator实例。在这里,完成了事件在topic上的注册和移除。

function Mediator() {
  if (!this instanceof Mediator) {
     return new Mediator();
  } else {
     this._topics = new Topic("");
  }
};

对于更高级的使用场景,我们可以让Mediator支持用于inbox:messages:new:read等主题topic的命名空间。在接下来的示例中,GetTopic根据命名空间返回相应的主题实例。

Mediator.prototype = {
   GetTopic: function( namespace ){
     var topic = this._topics,
          namespaceHierarchy = namespace.split( ":" );
     if( namespace === "" ){
       return topic;
     }
     if( namespaceHierarchy.length > 0 ){
      for( var i = 0, j = namespaceHierarchy.length; i < j; i++ ){
           if( !topic.HasTopic( namespaceHierarchy[i]) ){
              topic.AddTopic( namespaceHierarchy[i] );
           }
           topic = topic.ReturnTopic( namespaceHierarchy[i] );
       }
    }
    return topic;
  },

在本小节中,我们定义了Mediator.Subscribe方法,它接受一个topic命名空间、一个可执行的fn函数、options,以及调用该函数的context上下文。如果topic不存在,则创建一个。

Subscribe: function( topiclName, fn, options, context ){
  var options = options || {},
       context = context || {},
       topic = this.GetTopic( topicName ),
       sub = topic.AddSubscriber( fn, options, context );
  return sub;
},

继续下去,我们可以进一步定义用于访问特定订阅者或将订阅者从topic中递归删除的实用程序。

// 通过给定的订阅者ID/命名函数和topic命名空间返回一个订阅者

GetSubscriber: function( identifier, topic ){
   return this.GetTopic( topic || "" ).GetSubscriber( identifier );
},

// 通过给定的订阅者ID或命名函数,从给定的topic命名空间递归删除订阅者

Remove: function( topicName, identifier ){
this.GetTopic( topicName ).RemoveSubscriber( identifier );
},

主要的Publish方法允许我们向所选择的topic命名空间任意发布数据。

Topic向下递归调用。例如,一个发往inbox:messages的帖子将被发至inbox:messages: new和inbox:messages:new:read。如下所示:

Mediator.Publish( "inbox:messages:new", [args] );
Publish: function( topicName ){
  var args = Array.prototype.slice.call( arguments, 1),
       topic = this.GetTopic( topicName );
  args.push( topic );
  this.GetTopic( topicName ).Publish( args );
}
  };

最后,我们可以很容易地将Mediator作为一个对象附加到root上:

root.Mediator = Mediator;
Mediator.Topic = Topic;
Mediator.Subscriber = Subscriber;
// 记住,这里可以传递任何内容。这里我传递了window对象作为Mediator的附加对象,但也可以随时附加到其他对象上。
})( window );

9.6.3 示例
通过使用上述的任一种实现(简单的和高级的实现),我们可以建立一个简单的聊天记录系统,如下所示。

如下是HTML代码:
screenshot

如下是JavaScript代码:

$("#chatForm").on("submit", function (e) {
    e.preventDefault();
   // 从UI上获取chat的数据
   var text = $("#chatBox").val(),
       from = $("#fromBox").val();
       to = $("#toBox").val();

// 将数据发布到newMessage主题上

mediator.publish("newMessage", { message: text, from: from, to: to });
});

// 将新消息附加到聊天结果记录上

function displayChat(data) {
     var date = new Date(),
         msg = data.from + " said \"" + data.message + "\" to " + data.to;
    $("#chatResult")
    .prepend("" + msg + " (" + date.toLocaleTimeString() + ")");
}

// 记录消息日志

function logChat(data) {
     if (window.console) {
        console.log(data);
     }
}

// 通过mediator订阅新提交的newMessage主题

mediator.subscribe("newMessage", displayChat);
mediator.subscribe("newMessage", logChat);

// 如下代码仅在高级代码实现上可以使用

function amITalkingToMyself(data) {
     return data.from === data.to;
}
function iAmClearlyCrazy(data) {
    $("#chatResult").prepend("" + data.from + " is talking to himself.");
}
mediator.Subscribe(amITalkingToMyself, iAmClearlyCrazy);

9.6.4 优点和缺点
Mediator模式的最大好处是:它能够将系统中对象或组件之间所需的通信渠道从多对多减少到多对一。由于现在的解耦程度较高,添加新发布者和订阅者相对也容易多了。

或许使用这种模式最大的缺点是:它会引入单一故障点。将Mediator放置于模块之间可以导致性能下降,因为它们总是间接地进行通信。由于松耦合的性质,很难通过仅关注广播来确定一个系统如何作出反应。

也就是说,自我提醒解耦的系统有很多其他的优点:如果模块之间直接相互通信,模块的改变(如另一个模块抛出一个异常)容易让应用程序的其余部分产生多米诺效应。这个问题对解耦的系统来说就不是个大问题。

最后,紧密耦合会引起各种各样的问题,这只是另一个替代方案,但如果正确地实现,它也能很好地工作。

9.6.5 中介者(Mediator)与观察者(Observer)
开发人员通常想知道Mediator模式和Observer模式之间的差异是什么。无可否认地,它们之间有一些重叠,让我们重新参考“四人组”作出的解释:

在Observer模式中,不存在封装约束的单一对象。Observer和Subject(目标)必须合作才能维持约束。Communication(通信)模式由观察者和目标互连的方式所决定:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。
Mediator和Observer都能够促进松耦合;然而,Mediator模式通过限制对象严格通过Mediator进行通信来实现这一目的。Observer模式创建观察者对象,观察者对象向订阅它们的对象发布其感兴趣的事件。

9.6.6 中介者(Mediator)与外观(Facade)
我们将简单提一下Facade模式,但出于引用目的,一些开发人员可能也想知道Mediator和Facade模式之间是否有相似点。它们都能够抽象出现有模块的功能,但是也有一些细微的差别。

Mediator模块在它被模块显式引用的地方汇集这些模块之间的通信。从某种意义上说,这是多方向的。另一方面,Facade模式仅是为模块或系统定义了一个较简单的接口,而没有添加任何额外的功能。系统中的其他模块不会直接关联外观,所以可以被视为单向的。

相关文章
|
19天前
|
存储 安全 JavaScript
云计算浪潮中的网络安全之舵探索Node.js中的异步编程模式
【8月更文挑战第27天】在数字化时代的风帆下,云计算如同一片广阔的海洋,承载着企业与个人的数据梦想。然而,这片海洋并非总是风平浪静。随着网络攻击的波涛汹涌,如何确保航行的安全成为了每一个船员必须面对的挑战。本文将探索云计算环境下的网络安全策略,从云服务的本质出发,深入信息安全的核心,揭示如何在云海中找到安全的灯塔。
|
1天前
|
设计模式 算法 安全
设计模式——模板模式
模板方法模式、钩子方法、Spring源码AbstractApplicationContext类用到的模板方法
设计模式——模板模式
|
2天前
|
JavaScript 前端开发 中间件
深入浅出Node.js中间件模式
【9月更文挑战第13天】本文将带你领略Node.js中间件模式的魅力,从概念到实战,一步步揭示如何利用这一强大工具简化和增强你的Web应用。我们将通过实际代码示例,展示中间件如何在不修改原有代码的情况下,为请求处理流程添加功能层。无论你是前端还是后端开发者,这篇文章都将为你打开一扇通往更高效、更可维护代码的大门。
|
26天前
|
设计模式 JavaScript 前端开发
Vue.js组件设计模式:构建可复用组件库
在Vue.js中,构建可复用组件库是提升代码质量和维护性的核心策略。采用单文件组件(SFC),定义props及默认值,利用自定义事件和插槽进行灵活通信,结合Vuex或Pinia的状态管理,以及高阶组件技术,可以增强组件的功能性和灵活性。通过合理的抽象封装、考虑组件的可配置性和扩展性,并辅以详尽的文档和充分的测试,能够打造出既高效又可靠的组件库。此外,采用懒加载、按需导入技术优化性能,制定设计系统和风格指南确保一致性,配合版本控制、CI/CD流程和代码审查机制,最终形成一个高品质、易维护且具有良好社区支持的组件库。
48 7
|
24天前
|
设计模式 JavaScript 前端开发
Vue.js 组件设计模式:在前端热潮中找到归属感,打造可复用组件库,开启高效开发之旅!
【8月更文挑战第22天】Vue.js 以其高效构建单页应用著称,更可通过精良的组件设计打造可复用组件库。组件应职责单一、边界清晰,如一个显示文本并触发事件的按钮组件,通过 props 传递标签文本,利用插槽增强灵活性,允许父组件注入动态内容。结合 CSS 预处理器管理和封装独立模块,配以详尽文档,有效提升开发效率及代码可维护性。合理设计模式下,组件库既灵活又强大,持续实践可优化项目工作流。
36 1
|
28天前
|
设计模式 XML 存储
【二】设计模式~~~创建型模式~~~工厂方法模式(Java)
文章详细介绍了工厂方法模式(Factory Method Pattern),这是一种创建型设计模式,用于将对象的创建过程委托给多个工厂子类中的某一个,以实现对象创建的封装和扩展性。文章通过日志记录器的实例,展示了工厂方法模式的结构、角色、时序图、代码实现、优点、缺点以及适用环境,并探讨了如何通过配置文件和Java反射机制实现工厂的动态创建。
【二】设计模式~~~创建型模式~~~工厂方法模式(Java)
|
28天前
|
设计模式 XML Java
【一】设计模式~~~创建型模式~~~简单工厂模式(Java)
文章详细介绍了简单工厂模式(Simple Factory Pattern),这是一种创建型设计模式,用于根据输入参数的不同返回不同类的实例,而客户端不需要知道具体类名。文章通过图表类的实例,展示了简单工厂模式的结构、时序图、代码实现、优缺点以及适用环境,并提供了Java代码示例和扩展应用,如通过配置文件读取参数来实现对象的创建。
【一】设计模式~~~创建型模式~~~简单工厂模式(Java)
|
17天前
|
设计模式 JavaScript 前端开发
从工厂到单例再到策略:Vue.js高效应用JavaScript设计模式
【8月更文挑战第30天】在现代Web开发中,结合使用JavaScript设计模式与框架如Vue.js能显著提升代码质量和项目的可维护性。本文探讨了常见JavaScript设计模式及其在Vue.js中的应用。通过具体示例介绍了工厂模式、单例模式和策略模式的应用场景及其实现方法。例如,工厂模式通过`NavFactory`根据用户角色动态创建不同的导航栏组件;单例模式则通过全局事件总线`eventBus`实现跨组件通信;策略模式用于处理不同的表单验证规则。这些设计模式的应用不仅提高了代码的复用性和灵活性,还增强了Vue应用的整体质量。
13 0
|
25天前
|
JavaScript 前端开发 安全
TypeScript:解锁JavaScript的超级英雄模式!类型系统如何化身守护神,拯救你的代码免于崩溃与混乱,戏剧性变革开发体验!
【8月更文挑战第22天】TypeScript作为JavaScript的超集,引入了强大的类型系统,提升了编程的安全性和效率。本文通过案例展示TypeScript如何增强JavaScript:1) 显式类型声明确保函数参数与返回值的准确性;2) 接口和类加强类型检查,保证对象结构符合预期;3) 泛型编程提高代码复用性和灵活性。这些特性共同推动了前端开发的标准化和规模化。
46 0
|
28天前
|
设计模式
设计模式-单一职责模式
设计模式-单一职责模式