天天看點

觀察者設計模式

觀察者設計模式是一個好的設計模式,這個模式我們在開發中比較常見,尤其是它的變形模式訂閱/釋出者模式我們更是很熟悉,在我們所熟悉jQuery庫和vue.js架構中我們都有展現。我在面試中也曾經被問到observer和它的變形模式publish/subscribe,說實話,當時有點懵。随着工作經曆漸多,也認識到它的重要性,特别是當你想要朝着中進階工程師進階時這個東西更是繞不過。

定義

觀察者設計模式中有一個對象(被稱為subject)根據觀察者(observer)維護一個對象清單,自動通知它們對狀态的任何修改。

當一個subject要通知觀察者一些有趣的事情時,它會向觀察者發送通知(它可以包含通知主題相關的特定資料)

當我們不在希望某一特定的觀察員被通知它們所登記的主題變化時,這個主題可以将他們從觀察員名單上删除。

為了從整體上了解設計模式的用法和優勢,回顧已釋出的設計模式是非常有用的,這些設計模式的定義與語言無關。在GoF這本書中,觀察者設計模式是這樣定義的:

“一個或多個觀察者對某一subject的狀态感興趣,并通過附加它們自己來注冊它們對該主題的興趣。當觀察者可能感興趣的主題發生變化時,會發送一個通知資訊,該通知将調用們個觀察者中的更新方法。當觀察者不再對主題的狀态感興趣時,他們可以簡單地分離自己。”

組成

擴充我們所學,以元件形式實作observer模式:

主題(subject):維護一個觀察者清單,友善添加或删除觀察者

觀察者(observer):為需要通知對象更改狀态的對象提供一個更新接口

實際主題(ConcreteSubject):向觀察者發送關于狀态變化的通知,存儲實際觀察者的狀态

實際觀察者(ConcreteObserver):存儲引用到的實際主題,為觀察者實作一個更新接口,以確定狀态與主題的一緻。

實作

1.對一個subject可能擁有的觀察者清單進行模組化:

function ObserverList(){
  this.observerList = [];
}
 
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
 
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.indexOf = function( obj, startIndex ){
  var i = startIndex;
 
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
 
  return -1;
};
 
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};
           

2.對subject進行模組化,并在觀察者清單中補充添加、删除、通知觀察者的方法

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

3.為建立一個新的觀察者定義一個架構。架構中的update功能将被稍後的自定義行為覆寫

// The Observer
function Observer(){
  this.update = function(){
    // ...
  };
}
           

示例

使用上面定義的觀察者元件,我們做一個demo,定義如下:

  • 在頁面中添加新的可觀察複選框的按鈕;
  • 一個控制複選框将作為一個subject,通知其它的複選框,它們應該被檢查;
  • 正在被添加的複選框容器
  • 然後,我們定義實際的主題和實際的觀察者處理句柄,以便為頁面添加新的觀察者并實作更新接口。

執行個體代碼如下:

html

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

js

// 用extend()擴充一個對象
function extend( obj, extension ){
  for ( var key in extension ){
    obj[key] = extension[key];
  }
}
 
// DOM 元素的引用
var controlCheckbox = document.getElementById( "mainCheckbox" ),
  addBtn = document.getElementById( "addNewObserver" ),
  container = document.getElementById( "observersContainer" );
 
// 實際主題 (Concrete Subject)
// 将控制 checkbox 擴充到 Subject class
extend( controlCheckbox, new Subject() );
 
// 單擊checkbox 通知将發送到它的觀察者
controlCheckbox.onclick = function(){
  controlCheckbox.notify( controlCheckbox.checked );
};
 
addBtn.onclick = addNewObserver;
 
// 實際觀察者(Concrete Observer)
function addNewObserver(){
 
  // 新建立的checkbox被添加
  var check = document.createElement( "input" );
  check.type = "checkbox";
 
  // 擴充 checkbox 用 Observer class
  extend( check, new Observer() );
 
  // 用自定義的 update 行為覆寫預設的
  check.update = function( value ){
    this.checked = value;
  };
 
  // 添加新的 observer 到 observers 清單中
  // 為我們的 main subject
  controlCheckbox.addObserver( check );
 
  // Append the item to the container
  container.appendChild( check );
}
           

在這個示例中我們研究了如何實作和使用觀察者模式,涵蓋了主題(subject), 觀察者(observer),實際/具體對象(ConcreteSubject),實際/具體觀察者(ConcreteObserver)

效果示範:

demo

觀察者和釋出者訂閱模式之間的差異

雖然,觀察者模式很有用,但是在JavaScript中我們經常會用一種被稱為釋出/訂閱模式這種變體的觀察者模式。雖然它們很相似,但是這些模式之間還是有差別的。

觀察者模式要求希望接受主題通知的觀察者(或對象)必須訂閱該對象觸發事件的對象(主題)

然而,釋出/訂閱模式使用一個主題/事件通道,該通道位于希望接受通知(訂閱者)和觸發事件(釋出者)的對象之間。此事件系統允許代碼定義特定用于應用程式的事件,這些事件可以通過自定義參數來傳遞訂閱者所需的值。這樣的思路是為了避免訂閱者和釋出者的依賴關系。

與觀察者模式不同,它允許任何訂閱者實作一個适當的事件處理程式來注冊并接收釋出者釋出的主題通知。

下面一個例子提供了功能實作,使用釋出/訂閱模式,可以支援在幕後的publish(),subscribe(),unsubscribe()

// 一個簡單的郵件處理程式
// 接收郵件數
var mailCounter = 0;
 
// 初始化監聽主題的名為 "inbox/newMessage" 的訂閱者.
 
// 呈現一個新消息的預覽
var subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) {
 
  // 為了調試目的列印 topic
  console.log( "A new message was received: ", topic );
 
  // 使用從我們的主題傳遞的資料并向訂閱者顯示消息預覽
  $( ".messageSender" ).html( data.sender );
  $( ".messagePreview" ).html( data.body );
 
});
 
// 這是另一個訂閱者使用相同資料執行不同的任務.
 
// 更新計數器,顯示通過釋出者釋出所就收的消息數量
 
var subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) {
 
  $('.newMessageCounter').html( ++mailCounter );
 
});
 
publish( "inbox/newMessage", [{
  sender: "[email protected]",
  body: "Hey there! How are you doing today?"
}]);
 
// 我們可以在取消訂閱讓我們的訂閱者不能接收到任何新的主題通知如下:
// unsubscribe( subscriber1 );
// unsubscribe( subscriber2 );
           

它的用來促進松散耦合。它們不是直接調用其他對象的方法,而是訂閱另一個對象的特定任務或活動,并在發生改變時得到通知。

優勢

觀察者和釋出/訂閱模式鼓勵我們認真考慮應用程式的不同部分之間的關系。他們還幫助我們确定那些層次包含了直接關系,而那些層次則可以替換為一系列的主題和觀察者。這可以有效地将應用程式分解為更小的、松散耦合的塊,以改進代碼管理和重用潛力。使用觀察者模式的進一步動機是,我們需要在不适用類緊密耦合的情況下保持相關對象間的一緻性。例如,當對象需要能夠通知其他對象是,不需要對這些對象進行假設。

在使用任何模式時,觀察者和主題之間都可以存在動态關系。這題懂了很大的靈活性,當我們的應用程式的不同部分緊密耦合時,實作的靈活性可能不那麼容易實作。

雖然它不一定是解決所有問題的最佳方案,但這些模式仍然是設計解耦系統的最佳工具之一,并且應該被認為是任何javascript開發人員的工具鍊中最重要的工具。

劣勢

這些模式的一些問題主要源于他們的好處。在釋出/訂閱模式中,通過将釋出者與訂閱者分離,有時很保證我們的應用程式的某些特定部分可以像我們預期的那樣運作。

例如,釋出者可能會假設一個或多個訂閱者正在監聽他們。假設我們使用這樣的假設來記錄或輸出一些應用程式的錯誤。如果執行日志記錄崩潰的訂閱者(或者由于某種原因不能正常運作),那麼由于系統的解耦特性,釋出者将無法看到這一點。

這種情況的另一種說法是,使用者不知道彼此的存在,對交換釋出者的成本視而不見。由于訂閱者和釋出者之間的動态關系,更新依賴關系可能很難跟蹤。

釋出/訂閱模式的實作

釋出/訂閱在JavaScript生态系統中很适用,這在很大程度上是因為在核心的ECMAScript實作是事件驅動的。在浏覽器環境中尤其如此,因為DOM将事件作為腳本的主要互動API。

也就是說,ECMAScript和DOM都不提供在實作代碼中建立自定義事件系統的核心對象或方法(可能隻有DOM3 CustomEvent,它是綁定到DOM的,不是通用)。

幸運的是,流行的JavaScript庫,如dojo、jQuery(自定義事件)和YUI已經有了一些實用工具,它們可以幫助輕松實作釋出/訂閱系統。下面我們可以看到一些例子:

var pubsub = {};

(function(myObject) {
 
    // Storage for topics that can be broadcast
    // or listened to
    var topics = {};
 
    // A topic identifier
    var subUid = -1;
 
    // Publish or broadcast events of interest
    // with a specific topic name and arguments
    // such as the data to pass along
    myObject.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
    myObject.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
    myObject.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 ));
           

簡單實作如下:

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

使用者接口通知

接下來我們假設有一個web應用程式負責顯示實時股票資訊。

應用程式可能有一個網格用于顯示股票統計資料和顯示最新更新點的計數器。當資料模型發生變化時,應用程式将需要更新網格和計數器。在這個場景中,我們的主題(将釋出主題/通知)是資料模型,我們的訂閱者是網格和計數器。

當我們的訂閱者收到通知時,模型本身已經更改,他們可以相應地更新自己。

在我們的實作中,我們的訂閱使用者将收主題“newDataAvailable”,以了解是否有新的股票資訊可用。如果一個新的通知釋出到這個主題,它将觸發gridUpdate向包含該資訊的網格添加一個新的行。它還将更新上一次更新的計數器,以記錄上一次添加的資料

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

其它設計模式相關文章請轉

‘大處着眼,小處着手’——設計模式系列

繼續閱讀