字典中中介者的定義是,一個(gè)中立方,在談判和沖突解決過程中起輔助作用。在我們的世界,一個(gè)中介者是一個(gè)行為設(shè)計(jì)模式,使我們可以導(dǎo)出統(tǒng)一的接口,這樣系統(tǒng)不同部分就可以彼此通信。
如果系統(tǒng)組件之間存在大量的直接關(guān)系,就可能是時(shí)候,使用一個(gè)中心的控制點(diǎn),來讓不同的組件通過它來通信。中介者通過將組件之間顯式的直接的引用替換成通過中心點(diǎn)來交互的方式,來做到松耦合。這樣可以幫助我們解耦,和改善組件的重用性。
在現(xiàn)實(shí)世界中,類似的系統(tǒng)就是,飛行控制系統(tǒng)。一個(gè)航站塔(中介者)處理哪個(gè)飛機(jī)可以起飛,哪個(gè)可以著陸,因?yàn)樗械耐ㄐ牛ūO(jiān)聽的通知或者廣播的通知)都是飛機(jī)和控制塔之間進(jìn)行的,而不是飛機(jī)和飛機(jī)之間進(jìn)行的。一個(gè)中央集權(quán)的控制中心是這個(gè)系統(tǒng)成功的關(guān)鍵,也正是中介者在軟件設(shè)計(jì)領(lǐng)域中所扮演的角色。
從實(shí)現(xiàn)角度來講,中介者模式是觀察者模式中的共享被觀察者對(duì)象。在這個(gè)系統(tǒng)中的對(duì)象之間直接的發(fā)布/訂閱關(guān)系被犧牲掉了,取而代之的是維護(hù)一個(gè)通信的中心節(jié)點(diǎn)。
也可以認(rèn)為是一種補(bǔ)充-用于應(yīng)用級(jí)別的通知,例如不同子系統(tǒng)之間的通信,子系統(tǒng)本身很復(fù)雜,可能需要使用發(fā)布/訂閱模式來做內(nèi)部組件之間的解耦。
另外一個(gè)類似的例子是DOM的事件冒泡機(jī)制,以及事件代理機(jī)制。如果系統(tǒng)中所有的訂閱者都是對(duì)文檔訂閱,而不是對(duì)獨(dú)立的節(jié)點(diǎn)訂閱,那么文檔就充當(dāng)一個(gè)中介者的角色。DOM的這種做法,不是將事件綁定到獨(dú)立節(jié)點(diǎn)上,而是用一個(gè)更高級(jí)別的對(duì)象負(fù)責(zé)通知訂閱者關(guān)于交互事件的信息。
中間人模式的一種簡(jiǎn)單的實(shí)現(xiàn)可以在下面找到,publish()和subscribe()方法都被暴露出來使用:
var mediator = (function(){
// Storage for topics that can be broadcast or listened to
var topics = {};
// Subscribe to a topic, supply a callback to be executed
// when that topic is broadcast to
var subscribe = function( topic, fn ){
if ( !topics[topic] ){
topics[topic] = [];
}
topics[topic].push( { context: this, callback: fn } );
return this;
};
// Publish/broadcast an event to the rest of the application
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;
}
};
}());
對(duì)于那些對(duì)更加高級(jí)實(shí)現(xiàn)感興趣的人,以走讀的方式看一看以下我對(duì)Jack Lawson優(yōu)秀的Mediator.js重寫的一個(gè)縮略版本.在其它方面的改進(jìn)當(dāng)中,為我們的中間人支持主題命名空間,用戶拆卸和一個(gè)更加穩(wěn)定的發(fā)布/訂閱系統(tǒng)。但是如果你想跳過這個(gè)走讀,你可以直接進(jìn)入到下一個(gè)例子繼續(xù)閱讀。
得感謝Jack優(yōu)秀的代碼注釋對(duì)這部分內(nèi)容的協(xié)助。
首先,讓我們實(shí)現(xiàn)認(rèn)購(gòu)的概念,我們可以考慮一個(gè)中間人主題的注冊(cè)。
通過生成對(duì)象實(shí)體,我們稍后能夠簡(jiǎn)單的更新認(rèn)購(gòu),而不需要去取消注冊(cè)然后重新注冊(cè)它們.認(rèn)購(gòu)可以寫成一個(gè)使用被稱作一個(gè)選項(xiàng)對(duì)象或者一個(gè)上下文環(huán)境的函數(shù)
// Pass in a context to attach our Mediator to.
// By default this will be the window object
(function( root ){
function guidGenerator() { /*..*/}
// Our Subscriber constructor
function Subscriber( fn, options, context ){
if ( !(this instanceof Subscriber) ) {
return new Subscriber( fn, context, options );
}else{
// guidGenerator() is a function that generates
// GUIDs for instances of our Mediators Subscribers so
// we can easily reference them later on. We're going
// to skip its implementation for brevity
this.id = guidGenerator();
this.fn = fn;
this.options = options;
this.context = context;
this.topic = null;
}
}
})();
在我們的中間人主題中包涵了一長(zhǎng)串的回調(diào)和子主題,當(dāng)中間人發(fā)布在我們中間人實(shí)體上被調(diào)用的時(shí)候被啟動(dòng).它也包含操作數(shù)據(jù)列表的方法
// Let's model the Topic.
// JavaScript lets us use a Function object as a
// conjunction of a prototype for use with the new
// object and a constructor function to be invoked.
function Topic( namespace ){
if ( !(this instanceof Topic) ) {
return new Topic( namespace );
}else{
this.namespace = namespace || "";
this._callbacks = [];
this._topics = [];
this.stopped = false;
}
}
// Define the prototype for our topic, including ways to
// add new subscribers or retrieve existing ones.
Topic.prototype = {
// Add a new subscriber
AddSubscriber: function( fn, options, context ){
var callback = new Subscriber( fn, options, context );
this._callbacks.push( callback );
callback.topic = this;
return callback;
},
...
我們的主題實(shí)體被當(dāng)做中間人調(diào)用的一個(gè)參數(shù)被傳遞.使用一個(gè)方便實(shí)用的calledStopPropagation()方法,回調(diào)就可以進(jìn)一步被傳播開來:
StopPropagation: function(){
this.stopped = true;
},
我們也能夠使得當(dāng)提供一個(gè)GUID的標(biāo)識(shí)符的時(shí)候檢索訂購(gòu)用戶更加容易:
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;
}
}
}
},
接著,在我們需要它們的情況下,我們也能夠提供添加新主題,檢查現(xiàn)有的主題或者檢索主題的簡(jiǎn)單方法:
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];
},
如果我們覺得不再需要它們了,我們也可以明確的刪除這些訂購(gòu)用戶.下面就是通過它的其子主題遞歸刪除訂購(gòu)用戶的代碼:
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--;
}
}
},
接著我們通過遞歸子主題將發(fā)布任意參數(shù)的能夠包含到訂購(gòu)服務(wù)對(duì)象中:
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;
}
};
接著我們暴露我們將主要交互的調(diào)節(jié)實(shí)體.這里它是通過注冊(cè)的并且從主題中刪除的事件來實(shí)現(xiàn)的
function Mediator() {
if ( !(this instanceof Mediator) ) {
return new Mediator();
}else{
this._topics = new Topic( "" );
}
};
想要更多先進(jìn)的用例,我們可以看看調(diào)解支持的主題命名空間,下面這樣的asinbox:messages:new:read.GetTopic 返回基于一個(gè)命名空間的主題實(shí)體。
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;
},
這一節(jié)我們定義了一個(gè)Mediator.Subscribe方法,它接受一個(gè)主題命名空間,一個(gè)將要被執(zhí)行的函數(shù),選項(xiàng)和又一個(gè)在訂閱中調(diào)用函數(shù)的上下文環(huán)境.這樣就創(chuàng)建了一個(gè)主題,如果這樣的一個(gè)主題存在的話
Subscribe: function( topiclName, fn, options, context ){
var options = options || {},
context = context || {},
topic = this.GetTopic( topicName ),
sub = topic.AddSubscriber( fn, options, context );
return sub;
},
根據(jù)這一點(diǎn),我們可以進(jìn)一步定義能夠訪問特定訂閱用戶,或者將他們從主題中遞歸刪除的工具
// Returns a subscriber for a given subscriber id / named function and topic namespace
GetSubscriber: function( identifier, topic ){
return this.GetTopic( topic || "" ).GetSubscriber( identifier );
},
// Remove a subscriber from a given topic namespace recursively based on
// a provided subscriber id or named function.
Remove: function( topicName, identifier ){
this.GetTopic( topicName ).RemoveSubscriber( identifier );
},
我們主要的發(fā)布方式可以讓我們隨意發(fā)布數(shù)據(jù)到選定的主題命名空間,這可以在下面的代碼中看到。
主題可以被向下遞歸.例如,一條對(duì)inbox:message的post將發(fā)送到inbox:message:new和inbox:message: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 );
}
};
最后,我們可以很容易的暴露我們的中間人,將它附著在傳遞到根中的對(duì)象上:
root.Mediator = Mediator;
Mediator.Topic = Topic;
Mediator.Subscriber = Subscriber;
// Remember we can pass anything in here. I've passed inwindowto
// attach the Mediator to, but we can just as easily attach it to another
// object if desired.
})( window );
無論是使用來自上面的實(shí)現(xiàn)(簡(jiǎn)單的選項(xiàng)和更加先進(jìn)的選項(xiàng)都是),我們能夠像下面這樣將一個(gè)簡(jiǎn)單的聊天記錄系統(tǒng)整到一起:
<h1>Chat</h1>
<form id="chatForm">
<label for="fromBox">Your Name:</label>
<input id="fromBox" type="text"/>
<br />
<label for="toBox">Send to:</label>
<input id="toBox" type="text"/>
<br />
<label for="chatBox">Message:</label>
<input id="chatBox" type="text"/>
<button type="submit">Chat</button>
</form>
<div id="chatResult"></div>
$( "#chatForm" ).on( "submit", function(e) {
e.preventDefault();
// Collect the details of the chat from our UI
var text = $( "#chatBox" ).val(),
from = $( "#fromBox" ).val(),
to = $( "#toBox" ).val();
// Publish data from the chat to the newMessage topic
mediator.publish( "newMessage" , { message: text, from: from, to: to } );
});
// Append new messages as they come through
function displayChat( data ) {
var date = new Date(),
msg = data.from + " said \"" + data.message + "\" to " + data.to;
$( "#chatResult" )
.prepend("
<p>
" + msg + " (" + date.toLocaleTimeString() + ")
</p>
");
}
// Log messages
function logChat( data ) {
if ( window.console ) {
console.log( data );
}
}
// Subscribe to new chat messages being submitted
// via the mediator
mediator.subscribe( "newMessage", displayChat );
mediator.subscribe( "newMessage", logChat );
// The following will however only work with the more advanced implementation:
function amITalkingToMyself( data ) {
return data.from === data.to;
}
function iAmClearlyCrazy( data ) {
$( "#chatResult" ).prepend("
<p>
" + data.from + " is talking to himself.
</p>
");
}
mediator.Subscribe( amITalkingToMyself, iAmClearlyCrazy );
中間人模式最大的好處就是,它節(jié)約了對(duì)象或者組件之間的通信信道,這些對(duì)象或者組件存在于從多對(duì)多到多對(duì)一的系統(tǒng)之中。由于解耦合水平的因素,添加新的發(fā)布或者訂閱者是相對(duì)容易的。
也許使用這個(gè)模式最大的缺點(diǎn)是它可以引入一個(gè)單點(diǎn)故障。在模塊之間放置一個(gè)中間人也可能會(huì)造成性能損失,因?yàn)樗鼈兘?jīng)常是間接地的進(jìn)行通信的。由于松耦合的特性,僅僅盯著廣播很難去確認(rèn)系統(tǒng)是如何做出反應(yīng)的。
這就是說,提醒我們自己解耦合的系統(tǒng)擁有許多其它的好處,是很有用的——如果我們的模塊互相之間直接的進(jìn)行通信,對(duì)于模塊的改變(例如:另一個(gè)模塊拋出了異常)可以很容易的對(duì)我們系統(tǒng)的其它部分產(chǎn)生多米諾連鎖效應(yīng)。這個(gè)問題在解耦合的系統(tǒng)中很少需要被考慮到。
在一天結(jié)束的時(shí)候,緊耦合會(huì)導(dǎo)致各種頭痛,這僅僅只是另外一種可選的解決方案,但是如果得到正確實(shí)現(xiàn)的話也能夠工作得很好。
開發(fā)人員往往不知道中間人模式和觀察者模式之間的區(qū)別。不可否認(rèn),這兩種模式之間有一點(diǎn)點(diǎn)重疊,但讓我們回過頭來重新尋求GoF的一種解釋:
“在觀察者模式中,沒有封裝約束的單一對(duì)象”。取而代之,觀察者和主題必須合作來維護(hù)約束。通信的模式?jīng)Q定于觀察者和主題相互關(guān)聯(lián)的方式:一個(gè)單獨(dú)的主題經(jīng)常有許多的觀察者,而有時(shí)候一個(gè)主題的觀察者是另外一個(gè)觀察者的主題。“
中間人和觀察者都提倡松耦合,然而,中間人默認(rèn)使用讓對(duì)象嚴(yán)格通過中間人進(jìn)行通信的方式實(shí)現(xiàn)松耦合。觀察者模式則創(chuàng)建了觀察者對(duì)象,這些觀察者對(duì)象會(huì)發(fā)布觸發(fā)對(duì)象認(rèn)購(gòu)的感興趣的事件。
不久我們的描述就將涵蓋門面模式,但作為參考之用,一些開發(fā)者也想知道中間人和門面模式之間有哪些相似之處。它們都對(duì)模塊的功能進(jìn)行抽象,但有一些細(xì)微的差別。
中間人模式讓模塊之間集中進(jìn)行通信,它會(huì)被這些模塊明確的引用。門面模式卻只是為模塊或者系統(tǒng)定義一個(gè)更加簡(jiǎn)單的接口,但不添加任何額外的功能。系統(tǒng)中其他的模塊并不直接意識(shí)到門面的概念,而可以被認(rèn)為是單向的。
更多建議: