觀察者模式是這樣一種設(shè)計(jì)模式。一個(gè)被稱(chēng)作被觀察者的對(duì)象,維護(hù)一組被稱(chēng)為觀察者的對(duì)象,這些對(duì)象依賴(lài)于被觀察者,被觀察者自動(dòng)將自身的狀態(tài)的任何變化通知給它們。
當(dāng)一個(gè)被觀察者需要將一些變化通知給觀察者的時(shí)候,它將采用廣播的方式,這條廣播可能包含特定于這條通知的一些數(shù)據(jù)。
當(dāng)特定的觀察者不再需要接受來(lái)自于它所注冊(cè)的被觀察者的通知的時(shí)候,被觀察者可以將其從所維護(hù)的組中刪除。 在這里提及一下設(shè)計(jì)模式現(xiàn)有的定義很有必要。這個(gè)定義是與所使用的語(yǔ)言無(wú)關(guān)的。通過(guò)這個(gè)定義,最終我們可以更深層次地了解到設(shè)計(jì)模式如何使用以及其優(yōu)勢(shì)。在四人幫的《設(shè)計(jì)模式:可重用的面向?qū)ο筌浖脑亍愤@本書(shū)中,是這樣定義觀察者模式的:
一個(gè)或者更多的觀察者對(duì)一個(gè)被觀察者的狀態(tài)感興趣,將自身的這種興趣通過(guò)附著自身的方式注冊(cè)在被觀察者身上。當(dāng)被觀察者發(fā)生變化,而這種便可也是觀察者所關(guān)心的,就會(huì)產(chǎn)生一個(gè)通知,這個(gè)通知將會(huì)被送出去,最后將會(huì)調(diào)用每個(gè)觀察者的更新方法。當(dāng)觀察者不在對(duì)被觀察者的狀態(tài)感興趣的時(shí)候,它們只需要簡(jiǎn)單的將自身剝離即可。
我們現(xiàn)在可以通過(guò)實(shí)現(xiàn)一個(gè)觀察者模式來(lái)進(jìn)一步擴(kuò)展我們剛才所學(xué)到的東西。這個(gè)實(shí)現(xiàn)包含一下組件:
首先,讓我們對(duì)被觀察者可能有的一組依賴(lài)其的觀察者進(jìn)行建模:
function ObserverList(){
this.observerList = [];
}
ObserverList.prototype.Add = function( obj ){
return this.observerList.push( obj );
};
ObserverList.prototype.Empty = function(){
this.observerList = [];
};
ObserverList.prototype.Count = function(){
return this.observerList.length;
};
ObserverList.prototype.Get = function( index ){
if( index > -1 && index < this.observerList.length ){
return this.observerList[ index ];
}
};
ObserverList.prototype.Insert = function( obj, index ){
var pointer = -1;
if( index === 0 ){
this.observerList.unshift( obj );
pointer = index;
}else if( index === this.observerList.length ){
this.observerList.push( obj );
pointer = index;
}
return pointer;
};
ObserverList.prototype.IndexOf = function( obj, startIndex ){
var i = startIndex, pointer = -1;
while( i < this.observerList.length ){
if( this.observerList[i] === obj ){
pointer = i;
}
i++;
}
return pointer;
};
ObserverList.prototype.RemoveAt = function( index ){
if( index === 0 ){
this.observerList.shift();
}else if( index === this.observerList.length -1 ){
this.observerList.pop();
}
};
// Extend an object with an extension
function extend( extension, obj ){
for ( var key in extension ){
obj[key] = extension[key];
}
}
接著,我們對(duì)被觀察者以及其增加,刪除,通知在觀察者列表中的觀察者的能力進(jìn)行建模:
function Subject(){
this.observers = new ObserverList();
}
Subject.prototype.AddObserver = function( observer ){
this.observers.Add( observer );
};
Subject.prototype.RemoveObserver = function( observer ){
this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) );
};
Subject.prototype.Notify = function( context ){
var observerCount = this.observers.Count();
for(var i=0; i < observerCount; i++){
this.observers.Get(i).Update( context );
}
};
我們接著定義建立新的觀察者的一個(gè)框架。這里的update 函數(shù)之后會(huì)被具體的行為覆蓋。
// The Observer
function Observer(){
this.Update = function(){
// ...
};
}
在我們的樣例應(yīng)用里面,我們使用上面的觀察者組件,現(xiàn)在我們定義:
我們接著定義具體被觀察者和具體觀察者,用于給頁(yè)面增加新的觀察者,以及實(shí)現(xiàn)更新接口。通過(guò)查看下面的內(nèi)聯(lián)的注釋?zhuān)闱宄谖覀儤永械倪@些組件是如何工作的。
<button id="addNewObserver">Add New Observer checkbox</button>
<input id="mainCheckbox" type="checkbox"/>
<div id="observersContainer"></div>
// 我們DOM 元素的引用
var controlCheckbox = document.getElementById( "mainCheckbox" ),
addBtn = document.getElementById( "addNewObserver" ),
container = document.getElementById( "observersContainer" );
// 具體的被觀察者
//Subject 類(lèi)擴(kuò)展controlCheckbox 類(lèi)
extend( new Subject(), controlCheckbox );
//點(diǎn)擊checkbox 將會(huì)觸發(fā)對(duì)觀察者的通知
controlCheckbox["onclick"] = new Function( "controlCheckbox.Notify(controlCheckbox.checked)" );
addBtn["onclick"] = AddNewObserver;
// 具體的觀察者
function AddNewObserver(){
//建立一個(gè)新的用于增加的checkbox
var check = document.createElement( "input" );
check.type = "checkbox";
// 使用Observer 類(lèi)擴(kuò)展checkbox
extend( new Observer(), check );
// 使用定制的Update函數(shù)重載
check.Update = function( value ){
this.checked = value;
};
// 增加新的觀察者到我們主要的被觀察者的觀察者列表中
controlCheckbox.AddObserver( check );
// 將元素添加到容器的最后
container.appendChild( check );
}
在這個(gè)例子里面,我們看到了如何實(shí)現(xiàn)和配置觀察者模式,了解了被觀察者,觀察者,具體被觀察者,具體觀察者的概念。
觀察者模式確實(shí)很有用,但是在javascript時(shí)間里面,通常我們使用一種叫做發(fā)布/訂閱模式的變體來(lái)實(shí)現(xiàn)觀察者模式。這兩種模式很相似,但是也有一些值得注意的不同。
觀察者模式要求想要接受相關(guān)通知的觀察者必須到發(fā)起這個(gè)事件的被觀察者上注冊(cè)這個(gè)事件。
發(fā)布/訂閱模式使用一個(gè)主題/事件頻道,這個(gè)頻道處于想要獲取通知的訂閱者和發(fā)起事件的發(fā)布者之間。這個(gè)事件系統(tǒng)允許代碼定義應(yīng)用相關(guān)的事件,這個(gè)事件可以傳遞特殊的參數(shù),參數(shù)中包含有訂閱者所需要的值。這種想法是為了避免訂閱者和發(fā)布者之間的依賴(lài)性。
這種和觀察者模式之間的不同,使訂閱者可以實(shí)現(xiàn)一個(gè)合適的事件處理函數(shù),用于注冊(cè)和接受由發(fā)布者廣播的相關(guān)通知。
這里給出一個(gè)關(guān)于如何使用發(fā)布者/訂閱者模式的例子,這個(gè)例子中完整地實(shí)現(xiàn)了功能強(qiáng)大的publish(), subscribe() 和 unsubscribe()。
// 一個(gè)非常簡(jiǎn)單的郵件處理器
// 接受的消息的計(jì)數(shù)器
var mailCounter = 0;
// 初始化一個(gè)訂閱者,這個(gè)訂閱者監(jiān)聽(tīng)名叫"inbox/newMessage" 的頻道
// 渲染新消息的粗略信息
var subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) {
// 日志記錄主題,用于調(diào)試
console.log( "A new message was received: ", topic );
// 使用來(lái)自于被觀察者的數(shù)據(jù),用于給用戶(hù)展示一個(gè)消息的粗略信息
$( ".messageSender" ).html( data.sender );
$( ".messagePreview" ).html( data.body );
});
// 這是另外一個(gè)訂閱者,使用相同的數(shù)據(jù)執(zhí)行不同的任務(wù)
// 更細(xì)計(jì)數(shù)器,顯示當(dāng)前來(lái)自于發(fā)布者的新信息的數(shù)量
var subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) {
$('.newMessageCounter').html( mailCounter++ );
});
publish( "inbox/newMessage", [{
sender:"hello@google.com",
body: "Hey there! How are you doing today?"
}]);
// 在之后,我們可以讓我們的訂閱者通過(guò)下面的方式取消訂閱來(lái)自于新主題的通知
// unsubscribe( subscriber1, );
// unsubscribe( subscriber2 );
這個(gè)例子的更廣的意義是對(duì)松耦合的原則的一種推崇。不是一個(gè)對(duì)象直接調(diào)用另外一個(gè)對(duì)象的方法,而是通過(guò)訂閱另外一個(gè)對(duì)象的一個(gè)特定的任務(wù)或者活動(dòng),從而在這個(gè)任務(wù)或者活動(dòng)出現(xiàn)的時(shí)候的得到通知。
觀察者和發(fā)布/訂閱模式鼓勵(lì)人們認(rèn)真考慮應(yīng)用不同部分之間的關(guān)系,同時(shí)幫助我們找出這樣的層,該層中包含有直接的關(guān)系,這些關(guān)系可以通過(guò)一些列的觀察者和被觀察者來(lái)替換掉。這中方式可以有效地將一個(gè)應(yīng)用程序切割成小塊,這些小塊耦合度低,從而改善代碼的管理,以及用于潛在的代碼復(fù)用。
使用觀察者模式更深層次的動(dòng)機(jī)是,當(dāng)我們需要維護(hù)相關(guān)對(duì)象的一致性的時(shí)候,我們可以避免對(duì)象之間的緊密耦合。例如,一個(gè)對(duì)象可以通知另外一個(gè)對(duì)象,而不需要知道這個(gè)對(duì)象的信息。
兩種模式下,觀察者和被觀察者之間都可以存在動(dòng)態(tài)關(guān)系。這提供很好的靈活性,而當(dāng)我們的應(yīng)用中不同的部分之間緊密耦合的時(shí)候,是很難實(shí)現(xiàn)這種靈活性的。
盡管這些模式并不是萬(wàn)能的靈丹妙藥,這些模式仍然是作為最好的設(shè)計(jì)松耦合系統(tǒng)的工具之一,因此在任何的JavaScript 開(kāi)發(fā)者的工具箱里面,都應(yīng)該有這樣一個(gè)重要的工具。
事實(shí)上,這些模式的一些問(wèn)題實(shí)際上正是來(lái)自于它們所帶來(lái)的一些好處。在發(fā)布/訂閱模式中,將發(fā)布者共訂閱者上解耦,將會(huì)在一些情況下,導(dǎo)致很難確保我們應(yīng)用中的特定部分按照我們預(yù)期的那樣正常工作。
例如,發(fā)布者可以假設(shè)有一個(gè)或者多個(gè)訂閱者正在監(jiān)聽(tīng)它們。比如我們基于這樣的假設(shè),在某些應(yīng)用處理過(guò)程中來(lái)記錄或者輸出錯(cuò)誤日志。如果訂閱者執(zhí)行日志功能崩潰了(或者因?yàn)槟承┰虿荒苷9ぷ鳎?,因?yàn)橄到y(tǒng)本身的解耦本質(zhì),發(fā)布者沒(méi)有辦法感知到這些事情。
另外一個(gè)這種模式的缺點(diǎn)是,訂閱者對(duì)彼此之間存在沒(méi)有感知,對(duì)切換發(fā)布者的代價(jià)無(wú)從得知。因?yàn)橛嗛喺吆桶l(fā)布者之間的動(dòng)態(tài)關(guān)系,更新依賴(lài)也很能去追蹤。
發(fā)布/訂閱在JavaScript的生態(tài)系統(tǒng)中非常合適,主要是因?yàn)樽鳛楹诵牡腅CMAScript 實(shí)現(xiàn)是事件驅(qū)動(dòng)的。尤其是在瀏覽器環(huán)境下更是如此,因?yàn)镈OM使用事件作為其主要的用于腳本的交互API。
也就是說(shuō),無(wú)論是ECMAScript 還是DOM都沒(méi)有在實(shí)現(xiàn)代碼中提供核心對(duì)象或者方法用于創(chuàng)建定制的事件系統(tǒng)(DOM3 的CustomEvent是一個(gè)例外,這個(gè)事件綁定在DOM上,因此通常用處不大)。
幸運(yùn)的是,流行的JavaScript庫(kù)例如dojo, jQuery(定制事件)以及YUI已經(jīng)有相關(guān)的工具,可以幫助我們方便的實(shí)現(xiàn)一個(gè)發(fā)布/訂閱者系統(tǒng)。下面我們看一些例子。
// 發(fā)布
// jQuery: $(obj).trigger("channel", [arg1, arg2, arg3]);
$( el ).trigger( "/login", [{username:"test", userData:"test"}] );
// Dojo: dojo.publish("channel", [arg1, arg2, arg3] );
dojo.publish( "/login", [{username:"test", userData:"test"}] );
// YUI: el.publish("channel", [arg1, arg2, arg3]);
el.publish( "/login", {username:"test", userData:"test"} );
// 訂閱
// jQuery: $(obj).on( "channel", [data], fn );
$( el ).on( "/login", function( event ){...} );
// Dojo: dojo.subscribe( "channel", fn);
var handle = dojo.subscribe( "/login", function(data){..} );
// YUI: el.on("channel", handler);
el.on( "/login", function( data ){...} );
// 取消訂閱
// jQuery: $(obj).off( "channel" );
$( el ).off( "/login" );
// Dojo: dojo.unsubscribe( handle );
dojo.unsubscribe( handle );
// YUI: el.detach("channel");
el.detach( "/login" );
對(duì)于想要在vanilla Javascript(或者其它庫(kù))中使用發(fā)布/訂閱模式的人來(lái)講, AmplifyJS 包含了一個(gè)干凈的,庫(kù)無(wú)關(guān)的實(shí)現(xiàn),可以和任何庫(kù)或者工具箱一起使用。Radio.js, PubSubJS 或者 Pure JS PubSub 來(lái)自于 Peter Higgins 都有類(lèi)似的替代品值得研究。
尤其對(duì)于jQuery 開(kāi)發(fā)者來(lái)講,他們擁有很多其它的選擇,可以選擇大量的良好實(shí)現(xiàn)的代碼,從Peter Higgins 的jQuery插件到Ben Alman 在GitHub 上的(優(yōu)化的)發(fā)布/訂閱 jQuery gist。下面給出了這些代碼的鏈接。
從上面我們可以看到在javascript中有這么多種觀察者模式的實(shí)現(xiàn),讓我們看一下最小的一個(gè)版本的發(fā)布/訂閱模式實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)我放在github 上,叫做pubsubz。這個(gè)實(shí)現(xiàn)展示了發(fā)布,訂閱的核心概念,以及如何取消訂閱。
我之所以選擇這個(gè)代碼作為我們例子的基礎(chǔ),是因?yàn)檫@個(gè)代碼緊密貼合了方法簽名和實(shí)現(xiàn)方式,這種實(shí)現(xiàn)方式正是我想看到的javascript版本的經(jīng)典的觀察者模式所應(yīng)該有的樣子。
var pubsub = {};
(function(q) {
var topics = {},
subUid = -1;
// Publish or broadcast events of interest
// with a specific topic name and arguments
// such as the data to pass along
q.publish = function( topic, args ) {
if ( !topics[topic] ) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func( topic, args );
}
return this;
};
// Subscribe to events of interest
// with a specific topic name and a
// callback function, to be executed
// when the topic/event is observed
q.subscribe = function( topic, func ) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = ( ++subUid ).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
// Unsubscribe from a specific
// topic, based on a tokenized reference
// to the subscription
q.unsubscribe = function( token ) {
for ( var m in topics ) {
if ( topics[m] ) {
for ( var i = 0, j = topics[m].length; i < j; i++ ) {
if ( topics[m][i].token === token) {
topics[m].splice( i, 1 );
return token;
}
}
}
}
return this;
};
}( pubsub ));
我們現(xiàn)在可以使用發(fā)布實(shí)例和訂閱感興趣的事件,例如:
// Another simple message handler
// A simple message logger that logs any topics and data received through our
// subscriber
var messageLogger = function ( topics, data ) {
console.log( "Logging: " + topics + ": " + data );
};
// Subscribers listen for topics they have subscribed to and
// invoke a callback function (e.g messageLogger) once a new
// notification is broadcast on that topic
var subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );
// Publishers are in charge of publishing topics or notifications of
// interest to the application. e.g:
pubsub.publish( "inbox/newMessage", "hello world!" );
// or
pubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );
// or
pubsub.publish( "inbox/newMessage", {
sender: "hello@google.com",
body: "Hey again!"
});
// We cab also unsubscribe if we no longer wish for our subscribers
// to be notified
// pubsub.unsubscribe( subscription );
// Once unsubscribed, this for example won't result in our
// messageLogger being executed as the subscriber is
// no longer listening
pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );
接下來(lái),讓我們想象一下,我們有一個(gè)Web應(yīng)用程序,負(fù)責(zé)顯示實(shí)時(shí)股票信息。
應(yīng)用程序可能有一個(gè)表格顯示股票統(tǒng)計(jì)數(shù)據(jù)和一個(gè)計(jì)數(shù)器顯示的最后更新點(diǎn)。當(dāng)數(shù)據(jù)模型發(fā)生變化時(shí),應(yīng)用程序?qū)⑿枰卤砀窈陀?jì)數(shù)器。在這種情況下,我們的主題(這將發(fā)布主題/通知)是數(shù)據(jù)模型以及我們的訂閱者是表格和計(jì)數(shù)器。
當(dāng)我們的訂閱者收到通知:該模型本身已經(jīng)改變,他們自己可以進(jìn)行相應(yīng)的更新。
在我們的實(shí)現(xiàn)中,如果發(fā)現(xiàn)新的股票信息是可用的,我們的訂閱者將收聽(tīng)到的主題“新數(shù)據(jù)可用”。如果一個(gè)新的通知發(fā)布到該主題,那將觸發(fā)表格去添加一個(gè)包含此信息的新行。它也將更新最后更新計(jì)數(shù)器,記錄最后一次添加的數(shù)據(jù)
// Return the current local time to be used in our UI later
getCurrentTime = function (){
var date = new Date(),
m = date.getMonth() + 1,
d = date.getDate(),
y = date.getFullYear(),
t = date.toLocaleTimeString().toLowerCase();
return (m + "/" + d + "/" + y + " " + t);
};
// Add a new row of data to our fictional grid component
function addGridRow( data ) {
// ui.grid.addRow( data );
console.log( "updated grid component with:" + data );
}
// Update our fictional grid to show the time it was last
// updated
function updateCounter( data ) {
// ui.grid.updateLastChanged( getCurrentTime() );
console.log( "data last updated at: " + getCurrentTime() + " with " + data);
}
// Update the grid using the data passed to our subscribers
gridUpdate = function( topic, data ){
if ( data !== "undefined" ) {
addGridRow( data );
updateCounter( data );
}
};
// Create a subscription to the newDataAvailable topic
var subscriber = pubsub.subscribe( "newDataAvailable", gridUpdate );
// The following represents updates to our data layer. This could be
// powered by ajax requests which broadcast that new data is available
// to the rest of the application.
// Publish changes to the gridUpdated topic representing new entries
pubsub.publish( "newDataAvailable", {
summary: "Apple made $5 billion",
identifier: "APPL",
stockPrice: 570.91
});
pubsub.publish( "newDataAvailable", {
summary: "Microsoft made $20 million",
identifier: "MSFT",
stockPrice: 30.85
});
樣例:在下面這個(gè)電影評(píng)分的例子里面,我們使用Ben Alman的發(fā)布/訂閱實(shí)現(xiàn)來(lái)解耦應(yīng)用程序。我們使用Ben Alman的jQuery實(shí)現(xiàn),來(lái)展示如何解耦用戶(hù)界面。請(qǐng)注意,我們?nèi)绾巫龅教峤灰粋€(gè)評(píng)分,來(lái)產(chǎn)生一個(gè)發(fā)布信息,這個(gè)信息表明了當(dāng)前新的用戶(hù)和評(píng)分?jǐn)?shù)據(jù)可用。
剩余的工作留給訂閱者,由訂閱者來(lái)代理這些主題中的數(shù)據(jù)發(fā)生的變化。在我們的例子中,我們將新的數(shù)據(jù)壓入到現(xiàn)存的數(shù)組中,接著使用Underscore庫(kù)的template()方法來(lái)渲染模板。
<script id="userTemplate" type="text/html">
<li><%= name %></li>
</script>
<script id="ratingsTemplate" type="text/html">
<li><strong><%= title %></strong> was rated <%= rating %>/5</li>
</script>
<div id="container">
<div class="sampleForm">
<p>
<label for="twitter_handle">Twitter handle:</label>
<input type="text" id="twitter_handle" />
</p>
<p>
<label for="movie_seen">Name a movie you've seen this year:</label>
<input type="text" id="movie_seen" />
</p>
<p>
<label for="movie_rating">Rate the movie you saw:</label>
<select id="movie_rating">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5" selected>5</option>
</select>
</p>
<p>
<button id="add">Submit rating</button>
</p>
</div>
<div class="summaryTable">
<div id="users"><h3>Recent users</h3></div>
<div id="ratings"><h3>Recent movies rated</h3></div>
</div>
</div>
;(function( $ ) {
// Pre-compile templates and "cache" them using closure
var
userTemplate = _.template($( "#userTemplate" ).html()),
ratingsTemplate = _.template($( "#ratingsTemplate" ).html());
// Subscribe to the new user topic, which adds a user
// to a list of users who have submitted reviews
$.subscribe( "/new/user", function( e, data ){
if( data ){
$('#users').append( userTemplate( data ));
}
});
// Subscribe to the new rating topic. This is composed of a title and
// rating. New ratings are appended to a running list of added user
// ratings.
$.subscribe( "/new/rating", function( e, data ){
var compiledTemplate;
if( data ){
$( "#ratings" ).append( ratingsTemplate( data );
}
});
// Handler for adding a new user
$("#add").on("click", function( e ) {
e.preventDefault();
var strUser = $("#twitter_handle").val(),
strMovie = $("#movie_seen").val(),
strRating = $("#movie_rating").val();
// Inform the application a new user is available
$.publish( "/new/user", { name: strUser } );
// Inform the app a new rating is available
$.publish( "/new/rating", { title: strMovie, rating: strRating} );
});
})( jQuery );
樣例:解耦一個(gè)基于Ajax的jQuery應(yīng)用。
在我們最后的例子中,我們將從實(shí)用的角度來(lái)看一下如何在開(kāi)發(fā)早起使用發(fā)布/訂閱模式來(lái)解耦代碼,這樣可以幫助我們避免之后痛苦的重構(gòu)過(guò)程。
在Ajax重度依賴(lài)的應(yīng)用里面,我們常會(huì)見(jiàn)到這種情況,當(dāng)我們收到一個(gè)請(qǐng)求的響應(yīng)之后,我們希望能夠完成不僅僅一個(gè)特定的操作。我們可以簡(jiǎn)單的將所有請(qǐng)求后的邏輯加入到成功的回調(diào)函數(shù)里面,但是這樣做有一些問(wèn)題。
高度耦合的應(yīng)用優(yōu)勢(shì)會(huì)增加重用功能的代價(jià),因?yàn)楦叨锐詈显黾恿藘?nèi)部函數(shù)/代碼的依賴(lài)性。這意味著如果我們只是希望獲取一次性獲取結(jié)果集,可以將請(qǐng)求后 的邏輯代碼 硬編碼在回調(diào)函數(shù)里面,這種方式可以正常工作,但是當(dāng)我們想要對(duì)相同的數(shù)據(jù)源(不同的最終行為)做更多的Ajax調(diào)用的時(shí)候,這種方式就不適合了,我們必須要多次重寫(xiě)部分代碼。與其回溯調(diào)用相同數(shù)據(jù)源的每一層,然后在將它們泛化,不如一開(kāi)始就使用發(fā)布/訂閱模式來(lái)節(jié)約時(shí)間。
使用觀察者,我們可以簡(jiǎn)單的將整個(gè)應(yīng)用范圍的通知進(jìn)行隔離,針對(duì)不同的事件,我們可以把這種隔離做到我們想要的粒度上,如果使用其它模式,則可能不會(huì)有這么優(yōu)雅的實(shí)現(xiàn)。
注意我們下面的例子中,當(dāng)用戶(hù)表明他們想要做一次搜索查詢(xún)的時(shí)候,一個(gè)話(huà)題通知就會(huì)生成,而當(dāng)請(qǐng)求返回,并且實(shí)際的數(shù)據(jù)可用的時(shí)候,又會(huì)生成另外一個(gè)通知。而如何使用這些事件(或者返回的數(shù)據(jù)),都是由訂閱者自己決定的。這樣做的好處是,如果我們想要,我們可以有10個(gè)不同的訂閱者,以不同的方式使用返回的數(shù)據(jù),而對(duì)于Ajax層來(lái)講,它不會(huì)關(guān)心你如何處理數(shù)據(jù)。它唯一的責(zé)任就是請(qǐng)求和返回?cái)?shù)據(jù),接著將數(shù)據(jù)發(fā)送給所有想要使用數(shù)據(jù)的地方。這種相關(guān)性上的隔離可以是我們整個(gè)代碼設(shè)計(jì)更為清晰。
<form id="flickrSearch">
<input type="text" name="tag" id="query"/>
<input type="submit" name="submit" value="submit"/>
</form>
<div id="lastQuery"></div>
<div id="searchResults"></div>
<script id="resultTemplate" type="text/html">
<% _.each(items, function( item ){ %>
<li><p><img src="<%= item.media.m %>"/></p></li>
<% });%>
</script>
;(function( $ ) {
// Pre-compile template and "cache" it using closure
var resultTemplate = _.template($( "#resultTemplate" ).html());
// Subscribe to the new search tags topic
$.subscribe( "/search/tags" , function( tags ) {
$( "#searchResults" )
.html("
<p>
Searched for:<strong>" + tags + "</strong>
</p>
");
});
// Subscribe to the new results topic
$.subscribe( "/search/resultSet" , function( results ){
$( "#searchResults" ).append(resultTemplate( results ));
});
// Submit a search query and publish tags on the /search/tags topic
$( "#flickrSearch" ).submit( function( e ) {
e.preventDefault();
var tags = $(this).find( "#query").val();
if ( !tags ){
return;
}
$.publish( "/search/tags" , [ $.trim(tags) ]);
});
// Subscribe to new tags being published and perform
// a search query using them. Once data has returned
// publish this data for the rest of the application
// to consume
$.subscribe("/search/tags", function( tags ) {
$.getJSON( "http://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?" ,{
tags: tags,
tagmode: "any",
format: "json"
},
function( data ){
if( !data.items.length ) {
return;
}
$.publish( "/search/resultSet" , data.items );
});
});
})();
觀察者模式在應(yīng)用設(shè)計(jì)中,解耦一系列不同的場(chǎng)景上非常有用,如果你沒(méi)有用過(guò)它,我推薦你嘗試一下今天提到的之前寫(xiě)到的某個(gè)實(shí)現(xiàn)。這個(gè)模式是一個(gè)易于學(xué)習(xí)的模式,同時(shí)也是一個(gè)威力巨大的模式。