本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.6节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。
9.6 Mediator(中介者)模式
在字典里,中介者是指“协助谈判和解决冲突的中立方”1。在本书设计模式里,中介者是一种行为设计模式,它允许我们公开一个统一的接口,系统的不同部分可以通过该接口进行通信。
如果一个系统的各个组件之间看起来有太多的直接关系,也许是时候需要一个中心控制点了,以便各个组件可以通过这个中心控制点进行通信。Mediator模式促进松散耦合的方式是:确保组件的交互是通过这个中心点来处理的,而不是通过显式地引用彼此。这种模式可以帮助我们解耦系统并提高组件的可重用性。
现实世界的一个例子是典型的机场交通控制系统。机场控制塔(中介者)处理飞机的起飞和降落,因为所有通信(监听到或发出的通知)都是从飞机到控制塔,而不是飞机和飞机直接相互通信的。中央控制系统是该系统成功的关键,而这才是中介者在软件设计中所担任的真正角色(图9-5)。
就实现而言,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代码:
如下是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模式仅是为模块或系统定义了一个较简单的接口,而没有添加任何额外的功能。系统中的其他模块不会直接关联外观,所以可以被视为单向的。