鍍金池/ 教程/ HTML/ 觀察者模式
中介者模式
MVVM
亨元模式
設(shè)計(jì)模式分類(lèi)概覽表
ES Harmony
組合模式
CommonJS
jQuery 插件的設(shè)計(jì)模式
外觀模式
觀察者模式
建造者模式
構(gòu)造器模式
外觀模式
簡(jiǎn)介
AMD
原型模式
設(shè)計(jì)模式的分類(lèi)
觀察者模式
命名空間模式
代理模式
編寫(xiě)設(shè)計(jì)模式
適配器模式
反模式
什么是設(shè)計(jì)模式
模塊化模式
MVC
Mixin 模式
裝飾模式
設(shè)計(jì)模式的結(jié)構(gòu)
單例模式
迭代器模式
命令模式
工廠(chǎng)模式
MVP
暴露模塊模式
惰性初始模式

觀察者模式

觀察者模式是這樣一種設(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)包含一下組件:

  • 被觀察者:維護(hù)一組觀察者, 提供用于增加和移除觀察者的方法。
  • 觀察者:提供一個(gè)更新接口,用于當(dāng)被觀察者狀態(tài)變化時(shí),得到通知。
  • 具體的被觀察者:狀態(tài)變化時(shí)廣播通知給觀察者,保持具體的觀察者的信息。
  • 具體的觀察者:保持一個(gè)指向具體被觀察者的引用,實(shí)現(xiàn)一個(gè)更新接口,用于觀察,以便保證自身狀態(tài)總是和被觀察者狀態(tài)一致的。

首先,讓我們對(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)在我們定義:

  • 一個(gè)按鈕,這個(gè)按鈕用于增加新的充當(dāng)觀察者的選擇框到頁(yè)面上
  • 一個(gè)控制用的選擇框 , 充當(dāng)一個(gè)被觀察者,通知其它選擇框是否應(yīng)該被選中
  • 一個(gè)容器,用于放置新的選擇框

我們接著定義具體被觀察者和具體觀察者,用于給頁(yè)面增加新的觀察者,以及實(shí)現(xiàn)更新接口。通過(guò)查看下面的內(nèi)聯(lián)的注釋?zhuān)闱宄谖覀儤永械倪@些組件是如何工作的。

HTML

<button id="addNewObserver">Add New Observer checkbox</button>
<input id="mainCheckbox" type="checkbox"/>
<div id="observersContainer"></div>

Sample script

// 我們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)和配置觀察者模式,了解了被觀察者,觀察者,具體被觀察者,具體觀察者的概念。

觀察者模式和發(fā)布/訂閱模式的不同

觀察者模式確實(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í)候的得到通知。

優(yōu)勢(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è)重要的工具。

缺點(diǎn)

事實(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ā)布/訂閱實(shí)現(xiàn)

發(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)該有的樣子。

發(fā)布/訂閱實(shí)例

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 ));

示例:使用我們的實(shí)現(xiàn)

我們現(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?" );

例如:用戶(hù)界面通知

接下來(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)渲染模板。

HTML/模板

<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>

JavaScript

;(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ì)更為清晰。

HTML/Templates

<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>

JavaScript

;(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è)威力巨大的模式。

上一篇:反模式下一篇:迭代器模式