摘要:定義觀察者設(shè)計模式中有一個對象被稱為根據(jù)觀察者維護(hù)一個對象列表,自動通知它們對狀態(tài)的任何修改。與觀察者模式不同,它允許任何訂閱者實現(xiàn)一個適當(dāng)?shù)氖录幚沓绦騺碜圆⒔邮瞻l(fā)布者發(fā)布的主題通知。
觀察者設(shè)計模式是一個好的設(shè)計模式,這個模式我們在開發(fā)中比較常見,尤其是它的變形模式訂閱/發(fā)布者模式我們更是很熟悉,在我們所熟悉jQuery庫和vue.js框架中我們都有體現(xiàn)。我在面試中也曾經(jīng)被問到observer和它的變形模式publish/subscribe,說實話,當(dāng)時有點懵。隨著工作經(jīng)歷漸多,也認(rèn)識到它的重要性,特別是當(dāng)你想要朝著中高級工程師進(jìn)階時這個東西更是繞不過。
定義觀察者設(shè)計模式中有一個對象(被稱為subject)根據(jù)觀察者(observer)維護(hù)一個對象列表,自動通知它們對狀態(tài)的任何修改。
當(dāng)一個subject要通知觀察者一些有趣的事情時,它會向觀察者發(fā)送通知(它可以包含通知主題相關(guān)的特定數(shù)據(jù))
當(dāng)我們不在希望某一特定的觀察員被通知它們所登記的主題變化時,這個主題可以將他們從觀察員名單上刪除。
為了從整體上了解設(shè)計模式的用法和優(yōu)勢,回顧已發(fā)布的設(shè)計模式是非常有用的,這些設(shè)計模式的定義與語言無關(guān)。在GoF這本書中,觀察者設(shè)計模式是這樣定義的:
“一個或多個觀察者對某一subject的狀態(tài)感興趣,并通過附加它們自己來注冊它們對該主題的興趣。當(dāng)觀察者可能感興趣的主題發(fā)生變化時,會發(fā)送一個通知信息,該通知將調(diào)用們個觀察者中的更新方法。當(dāng)觀察者不再對主題的狀態(tài)感興趣時,他們可以簡單地分離自己?!?/p> 組成
擴(kuò)展我們所學(xué),以組件形式實現(xiàn)observer模式:
主題(subject):維護(hù)一個觀察者列表,方便添加或刪除觀察者
觀察者(observer):為需要通知對象更改狀態(tài)的對象提供一個更新接口
實際主題(ConcreteSubject):向觀察者發(fā)送關(guān)于狀態(tài)變化的通知,存儲實際觀察者的狀態(tài)
實際觀察者(ConcreteObserver):存儲引用到的實際主題,為觀察者實現(xiàn)一個更新接口,以確保狀態(tài)與主題的一致。
實現(xiàn)1.對一個subject可能擁有的觀察者列表進(jìn)行建模:
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進(jìn)行建模,并在觀察者列表中補(bǔ)充添加、刪除、通知觀察者的方法
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.為創(chuàng)建一個新的觀察者定義一個框架。框架中的update功能將被稍后的自定義行為覆蓋
// The Observer function Observer(){ this.update = function(){ // ... }; }示例
使用上面定義的觀察者組件,我們做一個demo,定義如下:
在頁面中添加新的可觀察復(fù)選框的按鈕;
一個控制復(fù)選框?qū)⒆鳛橐粋€subject,通知其它的復(fù)選框,它們應(yīng)該被檢查;
正在被添加的復(fù)選框容器
然后,我們定義實際的主題和實際的觀察者處理句柄,以便為頁面添加新的觀察者并實現(xiàn)更新接口。
實例代碼如下:
html
js
// 用extend()擴(kuò)展一個對象 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 擴(kuò)展到 Subject class extend( controlCheckbox, new Subject() ); // 單擊checkbox 通知將發(fā)送到它的觀察者 controlCheckbox.onclick = function(){ controlCheckbox.notify( controlCheckbox.checked ); }; addBtn.onclick = addNewObserver; // 實際觀察者(Concrete Observer) function addNewObserver(){ // 新創(chuàng)建的checkbox被添加 var check = document.createElement( "input" ); check.type = "checkbox"; // 擴(kuò)展 checkbox 用 Observer class extend( check, new Observer() ); // 用自定義的 update 行為覆蓋默認(rèn)的 check.update = function( value ){ this.checked = value; }; // 添加新的 observer 到 observers 列表中 // 為我們的 main subject controlCheckbox.addObserver( check ); // Append the item to the container container.appendChild( check ); }
在這個示例中我們研究了如何實現(xiàn)和使用觀察者模式,涵蓋了主題(subject), 觀察者(observer),實際/具體對象(ConcreteSubject),實際/具體觀察者(ConcreteObserver)
效果演示:demo
觀察者和發(fā)布者訂閱模式之間的差異雖然,觀察者模式很有用,但是在JavaScript中我們經(jīng)常會用一種被稱為發(fā)布/訂閱模式這種變體的觀察者模式。雖然它們很相似,但是這些模式之間還是有區(qū)別的。
觀察者模式要求希望接受主題通知的觀察者(或?qū)ο螅┍仨氂嗛喸搶ο笥|發(fā)事件的對象(主題)
然而,發(fā)布/訂閱模式使用一個主題/事件通道,該通道位于希望接受通知(訂閱者)和觸發(fā)事件(發(fā)布者)的對象之間。此事件系統(tǒng)允許代碼定義特定用于應(yīng)用程序的事件,這些事件可以通過自定義參數(shù)來傳遞訂閱者所需的值。這樣的思路是為了避免訂閱者和發(fā)布者的依賴關(guān)系。
與觀察者模式不同,它允許任何訂閱者實現(xiàn)一個適當(dāng)?shù)氖录幚沓绦騺碜圆⒔邮瞻l(fā)布者發(fā)布的主題通知。
下面一個例子提供了功能實現(xiàn),使用發(fā)布/訂閱模式,可以支持在幕后的publish(),subscribe(),unsubscribe()
// 一個簡單的郵件處理程序 // 接收郵件數(shù) var mailCounter = 0; // 初始化監(jiān)聽主題的名為 "inbox/newMessage" 的訂閱者. // 呈現(xiàn)一個新消息的預(yù)覽 var subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) { // 為了調(diào)試目的打印 topic console.log( "A new message was received: ", topic ); // 使用從我們的主題傳遞的數(shù)據(jù)并向訂閱者顯示消息預(yù)覽 $( ".messageSender" ).html( data.sender ); $( ".messagePreview" ).html( data.body ); }); // 這是另一個訂閱者使用相同數(shù)據(jù)執(zhí)行不同的任務(wù). // 更新計數(shù)器,顯示通過發(fā)布者發(fā)布所就收的消息數(shù)量 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 );
它的用來促進(jìn)松散耦合。它們不是直接調(diào)用其他對象的方法,而是訂閱另一個對象的特定任務(wù)或活動,并在發(fā)生改變時得到通知。
優(yōu)勢觀察者和發(fā)布/訂閱模式鼓勵我們認(rèn)真考慮應(yīng)用程序的不同部分之間的關(guān)系。他們還幫助我們確定那些層次包含了直接關(guān)系,而那些層次則可以替換為一系列的主題和觀察者。這可以有效地將應(yīng)用程序分解為更小的、松散耦合的塊,以改進(jìn)代碼管理和重用潛力。使用觀察者模式的進(jìn)一步動機(jī)是,我們需要在不適用類緊密耦合的情況下保持相關(guān)對象間的一致性。例如,當(dāng)對象需要能夠通知其他對象是,不需要對這些對象進(jìn)行假設(shè)。
在使用任何模式時,觀察者和主題之間都可以存在動態(tài)關(guān)系。這題懂了很大的靈活性,當(dāng)我們的應(yīng)用程序的不同部分緊密耦合時,實現(xiàn)的靈活性可能不那么容易實現(xiàn)。
雖然它不一定是解決所有問題的最佳方案,但這些模式仍然是設(shè)計解耦系統(tǒng)的最佳工具之一,并且應(yīng)該被認(rèn)為是任何javascript開發(fā)人員的工具鏈中最重要的工具。
劣勢這些模式的一些問題主要源于他們的好處。在發(fā)布/訂閱模式中,通過將發(fā)布者與訂閱者分離,有時很保證我們的應(yīng)用程序的某些特定部分可以像我們預(yù)期的那樣運行。
例如,發(fā)布者可能會假設(shè)一個或多個訂閱者正在監(jiān)聽他們。假設(shè)我們使用這樣的假設(shè)來記錄或輸出一些應(yīng)用程序的錯誤。如果執(zhí)行日志記錄崩潰的訂閱者(或者由于某種原因不能正常運行),那么由于系統(tǒng)的解耦特性,發(fā)布者將無法看到這一點。
這種情況的另一種說法是,用戶不知道彼此的存在,對交換發(fā)布者的成本視而不見。由于訂閱者和發(fā)布者之間的動態(tài)關(guān)系,更新依賴關(guān)系可能很難跟蹤。
發(fā)布/訂閱模式的實現(xiàn)發(fā)布/訂閱在JavaScript生態(tài)系統(tǒng)中很適用,這在很大程度上是因為在核心的ECMAScript實現(xiàn)是事件驅(qū)動的。在瀏覽器環(huán)境中尤其如此,因為DOM將事件作為腳本的主要交互API。
也就是說,ECMAScript和DOM都不提供在實現(xiàn)代碼中創(chuàng)建自定義事件系統(tǒng)的核心對象或方法(可能只有DOM3 CustomEvent,它是綁定到DOM的,不是通用)。
幸運的是,流行的JavaScript庫,如dojo、jQuery(自定義事件)和YUI已經(jīng)有了一些實用工具,它們可以幫助輕松實現(xiàn)發(fā)布/訂閱系統(tǒng)。下面我們可以看到一些例子:
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 ));
簡單實現(xiàn)如下:
// 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 });
用戶接口通知
接下來我們假設(shè)有一個web應(yīng)用程序負(fù)責(zé)顯示實時股票信息。
應(yīng)用程序可能有一個網(wǎng)格用于顯示股票統(tǒng)計數(shù)據(jù)和顯示最新更新點的計數(shù)器。當(dāng)數(shù)據(jù)模型發(fā)生變化時,應(yīng)用程序?qū)⑿枰戮W(wǎng)格和計數(shù)器。在這個場景中,我們的主題(將發(fā)布主題/通知)是數(shù)據(jù)模型,我們的訂閱者是網(wǎng)格和計數(shù)器。
當(dāng)我們的訂閱者收到通知時,模型本身已經(jīng)更改,他們可以相應(yīng)地更新自己。
在我們的實現(xiàn)中,我們的訂閱用戶將收主題“newDataAvailable”,以了解是否有新的股票信息可用。如果一個新的通知發(fā)布到這個主題,它將觸發(fā)gridUpdate向包含該信息的網(wǎng)格添加一個新的行。它還將更新上一次更新的計數(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 });
其它設(shè)計模式相關(guān)文章請轉(zhuǎn)‘大處著眼,小處著手’——設(shè)計模式系列
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/90564.html
摘要:關(guān)鍵概念理解觀察者設(shè)計模式中主要區(qū)分兩個概念觀察者指觀察者對象,也就是消息的訂閱者被觀察者指要觀察的目標(biāo)對象,也就是消息的發(fā)布者。 原文首發(fā)于微信公眾號:jzman-blog,歡迎關(guān)注交流! 最近補(bǔ)一下設(shè)計模式相關(guān)的知識,關(guān)于觀察者設(shè)計模式主要從以下幾個方面來學(xué)習(xí),具體如下: 什么是觀察者設(shè)計模式 關(guān)鍵概念理解 通知觀察者的方式 觀察者模式的實現(xiàn) 觀察者模式的優(yōu)缺點 使用場景 下面...
摘要:為了幫助灰太狼擺脫被老婆平底鍋抽的悲劇,發(fā)起了解救灰太狼的行動,必須要知道觀察者模式。持有觀察者對象的集合。設(shè)計模式源碼下載 相信大家都有看過《喜洋洋與灰太狼》,說的是灰太狼和羊族的斗爭,而每次的結(jié)果都是灰太狼一飛沖天,伴隨著一句我還會回來的......。為灰太狼感到悲哀,抓不到羊,在家也被老婆平底鍋虐待?;姨菫槭裁磿@么背? 很簡單,灰太狼本身就有暴露行蹤的屬性,羊咩咩就能知曉灰太...
摘要:實際上,設(shè)計模式就是通過面向?qū)ο蟮奶匦?,將這些角色解耦觀察者模式本質(zhì)上就是一種訂閱發(fā)布的模型,從邏輯上來說就是一對多的依賴關(guān)系。在添加一個觀察者時,把被主題被觀察者對象以構(gòu)造函數(shù)的形式給傳入了觀察者。 每個角色都對應(yīng)這一個類,比如觀察者模式,觀察者對應(yīng)著觀察者類,被觀察者對應(yīng)著被觀察者類。實際上,設(shè)計模式就是通過面向?qū)ο蟮奶匦?,將這些角色解耦 觀察者模式本質(zhì)上就是一種訂閱 / 發(fā)布的模...
摘要:總結(jié)一下從表面上看觀察者模式里,只有兩個角色觀察者被觀察者而發(fā)布訂閱模式,卻不僅僅只有發(fā)布者和訂閱者兩個角色,還有第三個角色經(jīng)紀(jì)人存在。參考鏈接觀察者模式發(fā)布訂閱模式 做了這么長時間的 菜鳥程序員 ,我好像還沒有寫過一篇關(guān)于設(shè)計模式的博客...咳咳...意外,純屬意外。所以,我決定,從這一刻起,我要把設(shè)計模式在從頭學(xué)習(xí)一遍,不然都對不起我這 菜鳥 的身份。那這次,就從觀察者模式開始好啦...
摘要:總結(jié)一下從表面上看觀察者模式里,只有兩個角色觀察者被觀察者而發(fā)布訂閱模式,卻不僅僅只有發(fā)布者和訂閱者兩個角色,還有第三個角色經(jīng)紀(jì)人存在。參考鏈接觀察者模式發(fā)布訂閱模式 做了這么長時間的 菜鳥程序員 ,我好像還沒有寫過一篇關(guān)于設(shè)計模式的博客...咳咳...意外,純屬意外。所以,我決定,從這一刻起,我要把設(shè)計模式在從頭學(xué)習(xí)一遍,不然都對不起我這 菜鳥 的身份。那這次,就從觀察者模式開始好啦...
閱讀 3608·2020-12-03 17:42
閱讀 2779·2019-08-30 15:54
閱讀 2233·2019-08-30 15:44
閱讀 579·2019-08-30 14:08
閱讀 980·2019-08-30 14:00
閱讀 1116·2019-08-30 13:46
閱讀 2796·2019-08-29 18:33
閱讀 2939·2019-08-29 14:11