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