JavaScript 觀察者模式

2018-08-02 16:25 更新

觀察者模式

觀察者模式是這樣一種設(shè)計(jì)模式。一個(gè)被稱作被觀察者的對(duì)象,維護(hù)一組被稱為觀察者的對(duì)象,這些對(duì)象依賴于被觀察者,被觀察者自動(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ū)ο筌浖脑亍愤@本書中,是這樣定義觀察者模式的:

一個(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ì)被觀察者可能有的一組依賴其的觀察者進(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)的注釋,搞清楚在我們樣例中的這些組件是如何工作的。

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 類擴(kuò)展controlCheckbox 類
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 類擴(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ā)布者之間的依賴性。

這種和觀察者模式之間的不同,使訂閱者可以實(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)聽名叫"inbox/newMessage" 的頻道

// 渲染新消息的粗略信息
var subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) {

  // 日志記錄主題,用于調(diào)試
  console.log( "A new message was received: ", topic );

  // 使用來(lái)自于被觀察者的數(shù)據(jù),用于給用戶展示一個(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 開發(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)聽它們。比如我們基于這樣的假設(shè),在某些應(yīng)用處理過(guò)程中來(lái)記錄或者輸出錯(cuò)誤日志。如果訂閱者執(zhí)行日志功能崩潰了(或者因?yàn)槟承┰虿荒苷9ぷ鳎驗(yàn)橄到y(tǒng)本身的解耦本質(zhì),發(fā)布者沒有辦法感知到這些事情。

另外一個(gè)這種模式的缺點(diǎn)是,訂閱者對(duì)彼此之間存在沒有感知,對(duì)切換發(fā)布者的代價(jià)無(wú)從得知。因?yàn)橛嗛喺吆桶l(fā)布者之間的動(dòng)態(tài)關(guān)系,更新依賴也很能去追蹤。

發(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都沒有在實(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.jsPubSubJS 或者 Pure JS PubSub 來(lái)自于 Peter Higgins 都有類似的替代品值得研究。

尤其對(duì)于jQuery 開發(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?" );

例如:用戶界面通知

接下來(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)新的股票信息是可用的,我們的訂閱者將收聽到的主題“新數(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)展示如何解耦用戶界面。請(qǐng)注意,我們?nèi)绾巫龅教峤灰粋€(gè)評(píng)分,來(lái)產(chǎn)生一個(gè)發(fā)布信息,這個(gè)信息表明了當(dāng)前新的用戶和評(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)看一下如何在開發(fā)早起使用發(fā)布/訂閱模式來(lái)解耦代碼,這樣可以幫助我們避免之后痛苦的重構(gòu)過(guò)程。

在Ajax重度依賴的應(yīng)用里面,我們常會(huì)見到這種情況,當(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ù)/代碼的依賴性。這意味著如果我們只是希望獲取一次性獲取結(jié)果集,可以將請(qǐng)求后 的邏輯代碼 硬編碼在回調(diào)函數(shù)里面,這種方式可以正常工作,但是當(dāng)我們想要對(duì)相同的數(shù)據(jù)源(不同的最終行為)做更多的Ajax調(diào)用的時(shí)候,這種方式就不適合了,我們必須要多次重寫部分代碼。與其回溯調(diào)用相同數(shù)據(jù)源的每一層,然后在將它們泛化,不如一開始就使用發(fā)布/訂閱模式來(lái)節(jié)約時(shí)間。

使用觀察者,我們可以簡(jiǎn)單的將整個(gè)應(yīng)用范圍的通知進(jìn)行隔離,針對(duì)不同的事件,我們可以把這種隔離做到我們想要的粒度上,如果使用其它模式,則可能不會(huì)有這么優(yōu)雅的實(shí)現(xiàn)。

注意我們下面的例子中,當(dāng)用戶表明他們想要做一次搜索查詢的時(shí)候,一個(gè)話題通知就會(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)景上非常有用,如果你沒有用過(guò)它,我推薦你嘗試一下今天提到的之前寫到的某個(gè)實(shí)現(xiàn)。這個(gè)模式是一個(gè)易于學(xué)習(xí)的模式,同時(shí)也是一個(gè)威力巨大的模式。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)