設(shè)計(jì)模式提供了軟件開(kāi)發(fā)過(guò)程中的一些最佳實(shí)踐,可以幫助我們解決常見(jiàn)的編程問(wèn)題,提高軟件的可維護(hù)性和可復(fù)用性,并使我們的代碼更加健壯和靈活。設(shè)計(jì)模式可以帶來(lái)以下好處:提高代碼的可讀性和可維護(hù)性、提高軟件的可復(fù)用性、提高開(kāi)發(fā)效率、提高系統(tǒng)的靈活性和可擴(kuò)展性。今天我們講一下觀察者模式的具體應(yīng)用。
觀察者模式是一種軟件設(shè)計(jì)模式,它允許一個(gè)對(duì)象(稱(chēng)為“主題”)管理其依賴(lài)項(xiàng)(稱(chēng)為“觀察者”),它定義了對(duì)象之間的一對(duì)多依賴(lài)關(guān)系,當(dāng)一個(gè)對(duì)象狀態(tài)發(fā)生改變時(shí),其相關(guān)依賴(lài)項(xiàng)將會(huì)自動(dòng)收到通知。這種模式提供了一種靈活的方式,將一個(gè)對(duì)象的狀態(tài)與依賴(lài)它的多個(gè)對(duì)象聯(lián)系起來(lái)。

在觀察者模式中,主題和觀察者之間建立了一種訂閱關(guān)系。主題負(fù)責(zé)維護(hù)其狀態(tài)并提供一個(gè)注冊(cè)表,用于存儲(chǔ)與其相關(guān)聯(lián)的觀察者對(duì)象。當(dāng)主題的狀態(tài)發(fā)生改變時(shí),它會(huì)自動(dòng)通知所有與之相關(guān)聯(lián)的觀察者,并傳遞相應(yīng)的參數(shù)。觀察者接收到通知后,可以執(zhí)行相應(yīng)的操作來(lái)響應(yīng)主題狀態(tài)的改變。
意圖
觀察者模式是一種行為設(shè)計(jì)模式,允許你定義一種訂閱機(jī)制,可在對(duì)象事件發(fā)生時(shí)通知多個(gè)“觀察”該對(duì)象的其他對(duì)象。
問(wèn)題
假如你有兩種類(lèi)型的對(duì)象:顧客和商店。顧客對(duì)某個(gè)特定品牌的產(chǎn)品非常感興趣(例如最新型號(hào)的 iPhone 手機(jī)),而該產(chǎn)品很快將會(huì)在商店里出售。
顧客可以每天來(lái)商店看看產(chǎn)品是否到貨。但如果商品尚未到貨時(shí),絕大多數(shù)來(lái)到商店的顧客都會(huì)空手而歸。
另一方面,每次新產(chǎn)品到貨時(shí),商店可以向所有顧客發(fā)送郵件(可能會(huì)被視為垃圾郵件)。這樣,部分顧客就無(wú)需反復(fù)前往商店了,但也可能會(huì)惹惱對(duì)新產(chǎn)品沒(méi)有興趣的其他顧客。
我們似乎遇到了一個(gè)矛盾:要么讓顧客浪費(fèi)時(shí)間檢查產(chǎn)品是否到貨,要么讓商店浪費(fèi)資源去通知沒(méi)有需求的顧客。
解決方案
擁有一些值得關(guān)注的狀態(tài)的對(duì)象通常被稱(chēng)為目標(biāo),由于它要將自身的狀態(tài)改變通知給其他對(duì)象,我們也將其稱(chēng)為發(fā)布者(publisher)。所有希望關(guān)注發(fā)布者狀態(tài)變化的其他對(duì)象被稱(chēng)為訂閱者(subscribers)。
觀察者模式建議你為發(fā)布者類(lèi)添加訂閱機(jī)制,讓每個(gè)對(duì)象都能訂閱或取消訂閱發(fā)布者事件流。不要害怕!這并不像聽(tīng)上去那么復(fù)雜。實(shí)際上,該機(jī)制包括 1)一個(gè)用于存儲(chǔ)訂閱者對(duì)象引用的列表成員變量;2)幾個(gè)用于添加或刪除該列表中訂閱者的公有方法。

訂閱機(jī)制允許對(duì)象訂閱事件通知。
現(xiàn)在,無(wú)論何時(shí)發(fā)生了重要的發(fā)布者事件,它都要遍歷訂閱者并調(diào)用其對(duì)象的特定通知方法。
實(shí)際應(yīng)用中可能會(huì)有十幾個(gè)不同的訂閱者類(lèi)跟蹤著同一個(gè)發(fā)布者類(lèi)的事件,你不會(huì)希望發(fā)布者與所有這些類(lèi)相耦合的。此外如果他人會(huì)使用發(fā)布者類(lèi),那么你甚至可能會(huì)對(duì)其中的一些類(lèi)一無(wú)所知。
因此,所有訂閱者都必須實(shí)現(xiàn)同樣的接口,發(fā)布者僅通過(guò)該接口與訂閱者交互。接口中必須聲明通知方法及其參數(shù),這樣發(fā)布者在發(fā)出通知時(shí)還能傳遞一些上下文數(shù)據(jù)。

發(fā)布者調(diào)用訂閱者對(duì)象中的特定通知方法來(lái)通知訂閱者。
如果你的應(yīng)用中有多個(gè)不同類(lèi)型的發(fā)布者,且希望訂閱者可兼容所有發(fā)布者,那么你甚至可以進(jìn)一步讓所有發(fā)布者遵循同樣的接口。該接口僅需描述幾個(gè)訂閱方法即可。這樣訂閱者就能在不與具體發(fā)布者類(lèi)耦合的情況下通過(guò)接口觀察發(fā)布者的狀態(tài)。
真實(shí)世界類(lèi)比
如果你訂閱了一份雜志或報(bào)紙,那就不需要再去報(bào)攤查詢(xún)新出版的刊物了。出版社(即應(yīng)用中的“發(fā)布者”)會(huì)在刊物出版后(甚至提前)直接將最新一期寄送至你的郵箱中。
出版社負(fù)責(zé)維護(hù)訂閱者列表,了解訂閱者對(duì)哪些刊物感興趣。當(dāng)訂閱者希望出版社停止寄送新一期的雜志時(shí),他們可隨時(shí)從該列表中退出。
我們看一段代碼示例,然后再通過(guò)示例進(jìn)行分析。在JavaScript中,我們可以使用原型或類(lèi)來(lái)實(shí)現(xiàn)觀察者模式。下面是一個(gè)使用原型的實(shí)現(xiàn)示例:
// 觀察者接口
var Observer = function() {};
Observer.prototype.update = function(data) {};
// 具體觀察者
var ConcreteObserver1 = function() {};
ConcreteObserver1.prototype = Object.create(Observer.prototype);
ConcreteObserver1.prototype.constructor = ConcreteObserver1;
ConcreteObserver1.prototype.update = function(data) {
console.log('ConcreteObserver1 received data: ' + data);
};
// 具體觀察者
var ConcreteObserver2 = function() {};
ConcreteObserver2.prototype = Object.create(Observer.prototype);
ConcreteObserver2.prototype.constructor = ConcreteObserver2;
ConcreteObserver2.prototype.update = function(data) {
console.log('ConcreteObserver2 received data: ' + data);
};
// 主題
var Subject = function() {
this.observers = [];
};
Subject.prototype.registerObserver = function(observer) {
this.observers.push(observer);
};
Subject.prototype.notifyObservers = function(data) {
for (var i = 0; i < this.observers.length; i++) {
this.observers[i].update(data);
}
};
// 具體主題
var ConcreteSubject = function() {};
ConcreteSubject.prototype = Object.create(Subject.prototype);
ConcreteSubject.prototype.constructor = ConcreteSubject;
ConcreteSubject.prototype.setState = function(data) {
this.notifyObservers(data);
};
在上面的代碼中,我們首先定義了一個(gè)Observer接口,它包含一個(gè)update方法。然后我們創(chuàng)建了兩個(gè)具體的觀察者ConcreteObserver1和ConcreteObserver2,它們都實(shí)現(xiàn)了Observer接口的update方法。接著我們定義了一個(gè)主題Subject,它包含一個(gè)觀察者數(shù)組和一個(gè)注冊(cè)方法registerObserver,以及一個(gè)通知方法notifyObservers。最后我們創(chuàng)建了一個(gè)具體主題ConcreteSubject,它繼承了Subject的原型并實(shí)現(xiàn)了一個(gè)setState方法,該方法調(diào)用通知方法來(lái)通知所有觀察者狀態(tài)改變。
在上面的示例中,我們使用了原型繼承來(lái)實(shí)現(xiàn)Observer接口和具體的觀察者類(lèi)。在實(shí)際應(yīng)用中,我們也可以使用類(lèi)繼承或ES6的class語(yǔ)法來(lái)實(shí)現(xiàn)這些類(lèi)。另外,在具體主題ConcreteSubject中,我們通過(guò)調(diào)用notifyObservers方法來(lái)通知所有觀察者狀態(tài)改變,這個(gè)方法可以傳遞一個(gè)參數(shù)作為通知的內(nèi)容。在具體觀察者的update方法中,我們可以根據(jù)傳遞的參數(shù)來(lái)執(zhí)行相應(yīng)的操作。
除了使用JavaScript實(shí)現(xiàn)觀察者模式外,我們還可以在其他編程語(yǔ)言和框架中找到這種模式的實(shí)現(xiàn)。例如,Redis的訂閱模型和WebSocket請(qǐng)求都使用了類(lèi)似的方式來(lái)實(shí)現(xiàn)主題和觀察者之間的訂閱關(guān)系。這些實(shí)現(xiàn)方式都允許客戶端訂閱特定主題,并在主題狀態(tài)發(fā)生改變時(shí)自動(dòng)接收通知。
觀察者模式適合應(yīng)用場(chǎng)景
當(dāng)一個(gè)對(duì)象狀態(tài)的改變需要改變其他對(duì)象,或?qū)嶋H對(duì)象是事先未知的或動(dòng)態(tài)變化的時(shí),可使用觀察者模式。
當(dāng)你使用圖形用戶界面類(lèi)時(shí)通常會(huì)遇到一個(gè)問(wèn)題。比如,你創(chuàng)建了自定義按鈕類(lèi)并允許客戶端在按鈕中注入自定義代碼,這樣當(dāng)用戶按下按鈕時(shí)就會(huì)觸發(fā)這些代碼。
觀察者模式允許任何實(shí)現(xiàn)了訂閱者接口的對(duì)象訂閱發(fā)布者對(duì)象的事件通知。你可在按鈕中添加訂閱機(jī)制,允許客戶端通過(guò)自定義訂閱類(lèi)注入自定義代碼。
當(dāng)應(yīng)用中的一些對(duì)象必須觀察其他對(duì)象時(shí),可使用該模式。但僅能在有限時(shí)間內(nèi)或特定情況下使用。
訂閱列表是動(dòng)態(tài)的,因此訂閱者可隨時(shí)加入或離開(kāi)該列表。
實(shí)現(xiàn)方式
-
仔細(xì)檢查你的業(yè)務(wù)邏輯,試著將其拆分為兩個(gè)部分:獨(dú)立于其他代碼的核心功能將作為發(fā)布者;其他代碼則將轉(zhuǎn)化為一組訂閱類(lèi)。
-
聲明訂閱者接口。該接口至少應(yīng)聲明一個(gè)
update方法。 -
聲明發(fā)布者接口并定義一些接口來(lái)在列表中添加和刪除訂閱對(duì)象。記住發(fā)布者必須僅通過(guò)訂閱者接口與它們進(jìn)行交互。
-
確定存放實(shí)際訂閱列表的位置并實(shí)現(xiàn)訂閱方法。通常所有類(lèi)型的發(fā)布者代碼看上去都一樣,因此將列表放置在直接擴(kuò)展自發(fā)布者接口的抽象類(lèi)中是顯而易見(jiàn)的。具體發(fā)布者會(huì)擴(kuò)展該類(lèi)從而繼承所有的訂閱行為。
但是,如果你需要在現(xiàn)有的類(lèi)層次結(jié)構(gòu)中應(yīng)用該模式,則可以考慮使用組合的方式:將訂閱邏輯放入一個(gè)獨(dú)立的對(duì)象,然后讓所有實(shí)際訂閱者使用該對(duì)象。
-
創(chuàng)建具體發(fā)布者類(lèi)。每次發(fā)布者發(fā)生了重要事件時(shí)都必須通知所有的訂閱者。
-
在具體訂閱者類(lèi)中實(shí)現(xiàn)通知更新的方法。絕大部分訂閱者需要一些與事件相關(guān)的上下文數(shù)據(jù)。這些數(shù)據(jù)可作為通知方法的參數(shù)來(lái)傳遞。
但還有另一種選擇。訂閱者接收到通知后直接從通知中獲取所有數(shù)據(jù)。在這種情況下,發(fā)布者必須通過(guò)更新方法將自身傳遞出去。另一種不太靈活的方式是通過(guò)構(gòu)造函數(shù)將發(fā)布者與訂閱者永久性地連接起來(lái)。
-
客戶端必須生成所需的全部訂閱者,并在相應(yīng)的發(fā)布者處完成注冊(cè)工作。
觀察者模式優(yōu)缺點(diǎn)
-
優(yōu)點(diǎn):
-
降低目標(biāo)與觀察者之間的耦合關(guān)系
-
支持“廣播通信”
-
符合開(kāi)閉原則。
-
-
確定:
-
通知可能會(huì)花費(fèi)很長(zhǎng)時(shí)間
-
循環(huán)依賴(lài)的問(wèn)題
-
沒(méi)有相應(yīng)的機(jī)制讓觀察者知道所觀察的目標(biāo)對(duì)象是怎么發(fā)生變化的
-
總結(jié)一下,觀察者模式適用于任何需要實(shí)現(xiàn)一對(duì)多依賴(lài)關(guān)系的場(chǎng)景,使得主題的狀態(tài)改變可以自動(dòng)通知給所有的觀察者。
-
訂閱/發(fā)布系統(tǒng):觀察者模式可以用于實(shí)現(xiàn)訂閱/發(fā)布系統(tǒng)。主題可以代表各種事件或消息,觀察者可以訂閱感興趣的主題并接收相關(guān)的通知。
-
實(shí)時(shí)通信:觀察者模式可以用于實(shí)現(xiàn)實(shí)時(shí)通信。當(dāng)某個(gè)事件發(fā)生時(shí),相關(guān)的觀察者可以立即得到通知并做出相應(yīng)的響應(yīng)。
-
數(shù)據(jù)綁定:在圖形用戶界面開(kāi)發(fā)中,觀察者模式可以用于實(shí)現(xiàn)數(shù)據(jù)綁定。當(dāng)某個(gè)數(shù)據(jù)源發(fā)生改變時(shí),相關(guān)的視圖可以自動(dòng)更新。
-
事件驅(qū)動(dòng)系統(tǒng):觀察者模式可以用于實(shí)現(xiàn)事件驅(qū)動(dòng)系統(tǒng)。當(dāng)某個(gè)事件觸發(fā)時(shí),相關(guān)的觀察者可以收到通知并執(zhí)行相應(yīng)的操作。
-
異步消息處理:在分布式系統(tǒng)中,觀察者模式可以用于實(shí)現(xiàn)異步消息處理。當(dāng)某個(gè)消息到達(dá)時(shí),相關(guān)的觀察者可以收到通知并處理該消息。
-
存儲(chǔ)
+關(guān)注
關(guān)注
13文章
4638瀏覽量
89207 -
軟件設(shè)計(jì)
+關(guān)注
關(guān)注
3文章
65瀏覽量
18222 -
代碼
+關(guān)注
關(guān)注
30文章
4927瀏覽量
72499 -
模式
+關(guān)注
關(guān)注
0文章
65瀏覽量
13734
原文標(biāo)題:觀察者模式,超詳細(xì)!
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
CC2540廣播者角色和觀察者角色切換代碼怎么編寫(xiě)?
RN4020觀察者模式無(wú)法正常工作怎么回事
屬性觀察者的特點(diǎn)
觀察者模式在嵌入式編程設(shè)計(jì)中有何作用
基于觀察者模式的屏幕布局控件設(shè)計(jì)
Java設(shè)計(jì)模式分析之觀察者
在 Java8 環(huán)境下實(shí)現(xiàn)觀察者模式的實(shí)例分析
大陸正在研發(fā)一款路況觀察者應(yīng)用
GoF設(shè)計(jì)模式之觀察者模式
設(shè)計(jì)模式行為型:觀察者模式
基于觀察者模式設(shè)計(jì)的框架-REB,使代碼模塊化
一文解析BLE觀察者模式回調(diào)機(jī)制
什么是觀察者設(shè)計(jì)模式?Golang中的觀察者模式介紹
觀察者網(wǎng):聚焦?RT-Thread睿賽德開(kāi)發(fā)者大會(huì)發(fā)布多個(gè)行業(yè)應(yīng)用操作系統(tǒng) | 媒體視角
基于藍(lán)牙模組Beacon+觀察者模式實(shí)現(xiàn)資產(chǎn)管理和室內(nèi)定位

觀察者模式,超詳細(xì)!
評(píng)論