# 中介者模式
字典中中介者的定義是,一個中立方,在談判和沖突解決過程中起輔助作用。在我們的世界,一個中介者是一個行為設計模式,使我們可以導出統一的接口,這樣系統不同部分就可以彼此通信。
如果系統組件之間存在大量的直接關系,就可能是時候,使用一個中心的控制點,來讓不同的組件通過它來通信。中介者通過將組件之間顯式的直接的引用替換成通過中心點來交互的方式,來做到松耦合。這樣可以幫助我們解耦,和改善組件的重用性。
在現實世界中,類似的系統就是,飛行控制系統。一個航站塔(中介者)處理哪個飛機可以起飛,哪個可以著陸,因為所有的通信(監聽的通知或者廣播的通知)都是飛機和控制塔之間進行的,而不是飛機和飛機之間進行的。一個中央集權的控制中心是這個系統成功的關鍵,也正是中介者在軟件設計領域中所扮演的角色。
從實現角度來講,中介者模式是觀察者模式中的共享被觀察者對象。在這個系統中的對象之間直接的發布/訂閱關系被犧牲掉了,取而代之的是維護一個通信的中心節點。
也可以認為是一種補充-用于應用級別的通知,例如不同子系統之間的通信,子系統本身很復雜,可能需要使用發布/訂閱模式來做內部組件之間的解耦。
另外一個類似的例子是DOM的事件冒泡機制,以及事件代理機制。如果系統中所有的訂閱者都是對文檔訂閱,而不是對獨立的節點訂閱,那么文檔就充當一個中介者的角色。DOM的這種做法,不是將事件綁定到獨立節點上,而是用一個更高級別的對象負責通知訂閱者關于交互事件的信息。
## 基礎的實現
中間人模式的一種簡單的實現可以在下面找到,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;
}
};
}());
~~~
## 高級的實現
對于那些對更加高級實現感興趣的人,以走讀的方式看一看以下我對Jack Lawson優秀的Mediator.js重寫的一個縮略版本.在其它方面的改進當中,為我們的中間人支持主題命名空間,用戶拆卸和一個更加穩定的發布/訂閱系統。但是如果你想跳過這個走讀,你可以直接進入到下一個例子繼續閱讀。
得感謝Jack優秀的代碼注釋對這部分內容的協助。
首先,讓我們實現認購的概念,我們可以考慮一個中間人主題的注冊。
通過生成對象實體,我們稍后能夠簡單的更新認購,而不需要去取消注冊然后重新注冊它們.認購可以寫成一個使用被稱作一個選項對象或者一個上下文環境的函數
~~~
// 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;
}
}
})();
~~~
在我們的中間人主題中包涵了一長串的回調和子主題,當中間人發布在我們中間人實體上被調用的時候被啟動.它也包含操作數據列表的方法
~~~
// 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;
},
...
~~~
我們的主題實體被當做中間人調用的一個參數被傳遞.使用一個方便實用的calledStopPropagation()方法,回調就可以進一步被傳播開來:
~~~
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;
}
}
}
},
~~~
接著,在我們需要它們的情況下,我們也能夠提供添加新主題,檢查現有的主題或者檢索主題的簡單方法:
~~~
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--;
}
}
},
~~~
接著我們通過遞歸子主題將發布任意參數的能夠包含到訂購服務對象中:
~~~
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;
}
};
~~~
接著我們暴露我們將主要交互的調節實體.這里它是通過注冊的并且從主題中刪除的事件來實現的
~~~
function Mediator() {
if ( !(this instanceof Mediator) ) {
return new Mediator();
}else{
this._topics = new Topic( "" );
}
};
~~~
想要更多先進的用例,我們可以看看調解支持的主題命名空間,下面這樣的asinbox:messages:new:read.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方法,它接受一個主題命名空間,一個將要被執行的函數,選項和又一個在訂閱中調用函數的上下文環境.這樣就創建了一個主題,如果這樣的一個主題存在的話
~~~
Subscribe: function( topiclName, fn, options, context ){
var options = options || {},
context = context || {},
topic = this.GetTopic( topicName ),
sub = topic.AddSubscriber( fn, options, context );
return sub;
},
~~~
根據這一點,我們可以進一步定義能夠訪問特定訂閱用戶,或者將他們從主題中遞歸刪除的工具
~~~
// 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 );
},
~~~
我們主要的發布方式可以讓我們隨意發布數據到選定的主題命名空間,這可以在下面的代碼中看到。
主題可以被向下遞歸.例如,一條對inbox:message的post將發送到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 );
}
};
~~~
最后,我們可以很容易的暴露我們的中間人,將它附著在傳遞到根中的對象上:
~~~
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 );
~~~
## 示例
無論是使用來自上面的實現(簡單的選項和更加先進的選項都是),我們能夠像下面這樣將一個簡單的聊天記錄系統整到一起:
### HTML
~~~
<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>
~~~
### Javascript
~~~
$( "#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 );
~~~
## 優點&缺點
中間人模式最大的好處就是,它節約了對象或者組件之間的通信信道,這些對象或者組件存在于從多對多到多對一的系統之中。由于解耦合水平的因素,添加新的發布或者訂閱者是相對容易的。
也許使用這個模式最大的缺點是它可以引入一個單點故障。在模塊之間放置一個中間人也可能會造成性能損失,因為它們經常是間接地的進行通信的。由于松耦合的特性,僅僅盯著廣播很難去確認系統是如何做出反應的。
這就是說,提醒我們自己解耦合的系統擁有許多其它的好處,是很有用的——如果我們的模塊互相之間直接的進行通信,對于模塊的改變(例如:另一個模塊拋出了異常)可以很容易的對我們系統的其它部分產生多米諾連鎖效應。這個問題在解耦合的系統中很少需要被考慮到。
在一天結束的時候,緊耦合會導致各種頭痛,這僅僅只是另外一種可選的解決方案,但是如果得到正確實現的話也能夠工作得很好。
## 中間人VS觀察者
開發人員往往不知道中間人模式和觀察者模式之間的區別。不可否認,這兩種模式之間有一點點重疊,但讓我們回過頭來重新尋求GoF的一種解釋:
“在觀察者模式中,沒有封裝約束的單一對象”。取而代之,觀察者和主題必須合作來維護約束。通信的模式決定于觀察者和主題相互關聯的方式:一個單獨的主題經常有許多的觀察者,而有時候一個主題的觀察者是另外一個觀察者的主題。“
中間人和觀察者都提倡松耦合,然而,中間人默認使用讓對象嚴格通過中間人進行通信的方式實現松耦合。觀察者模式則創建了觀察者對象,這些觀察者對象會發布觸發對象認購的感興趣的事件。
## 中間人VS門面
不久我們的描述就將涵蓋門面模式,但作為參考之用,一些開發者也想知道中間人和門面模式之間有哪些相似之處。它們都對模塊的功能進行抽象,但有一些細微的差別。
中間人模式讓模塊之間集中進行通信,它會被這些模塊明確的引用。門面模式卻只是為模塊或者系統定義一個更加簡單的接口,但不添加任何額外的功能。系統中其他的模塊并不直接意識到門面的概念,而可以被認為是單向的。
- 前言
- 簡介
- 什么是設計模式?
- 設計模式的結構
- 編寫設計模式
- 反模式
- 設計模式的分類
- 設計模式分類概覽表
- JavaScript 設計模式
- 構造器模式
- 模塊化模式
- 暴露模塊模式
- 單例模式
- 觀察者模式
- 中介者模式
- 原型模式
- 命令模式
- 外觀模式
- 工廠模式
- Mixin 模式
- 裝飾模式
- 亨元(Flyweight)模式
- JavaScript MV* 模式
- MVC 模式
- MVP 模式
- MVVM 模式
- 最新的模塊化 JavaScript 設計模式
- AMD
- CommonJS
- ES Harmony
- JQuery 中的設計模式
- 組合模式
- 適配器模式
- 外觀模式
- 觀察者模式
- 迭代器模式
- 惰性初始模式
- 代理模式
- 建造者模式
- jQuery 插件的設計模式
- JavaScript 命名空間模式
- 總結
- 參考