Go設(shè)計(jì)模式之觀察者模式圖解
意圖
觀察者模式是一種行為設(shè)計(jì)模式, 允許你定義一種訂閱機(jī)制, 可在對象事件發(fā)生時(shí)通知多個(gè) “觀察” 該對象的其他對象。
問題
假如你有兩種類型的對象: 顧客
和 商店
。 顧客對某個(gè)特定品牌的產(chǎn)品非常感興趣 (例如最新型號的 iPhone 手機(jī)), 而該產(chǎn)品很快將會(huì)在商店里出售。
顧客可以每天來商店看看產(chǎn)品是否到貨。 但如果商品尚未到貨時(shí), 絕大多數(shù)來到商店的顧客都會(huì)空手而歸。
前往商店和發(fā)送垃圾郵件
另一方面, 每次新產(chǎn)品到貨時(shí), 商店可以向所有顧客發(fā)送郵件 (可能會(huì)被視為垃圾郵件)。 這樣, 部分顧客就無需反復(fù)前往商店了, 但也可能會(huì)惹惱對新產(chǎn)品沒有興趣的其他顧客。
我們似乎遇到了一個(gè)矛盾: 要么讓顧客浪費(fèi)時(shí)間檢查產(chǎn)品是否到貨, 要么讓商店浪費(fèi)資源去通知沒有需求的顧客。
解決方案
擁有一些值得關(guān)注的狀態(tài)的對象通常被稱為目標(biāo), 由于它要將自身的狀態(tài)改變通知給其他對象, 我們也將其稱為發(fā)布者 (publisher)。 所有希望關(guān)注發(fā)布者狀態(tài)變化的其他對象被稱為訂閱者 (subscribers)。
觀察者模式建議你為發(fā)布者類添加訂閱機(jī)制, 讓每個(gè)對象都能訂閱或取消訂閱發(fā)布者事件流。 不要害怕! 這并不像聽上去那么復(fù)雜。 實(shí)際上, 該機(jī)制包括
1) 一個(gè)用于存儲訂閱者對象引用的列表成員變量;
2) 幾個(gè)用于添加或刪除該列表中訂閱者的公有方法;
訂閱機(jī)制允許對象訂閱事件通知。
現(xiàn)在, 無論何時(shí)發(fā)生了重要的發(fā)布者事件, 它都要遍歷訂閱者并調(diào)用其對象的特定通知方法。
實(shí)際應(yīng)用中可能會(huì)有十幾個(gè)不同的訂閱者類跟蹤著同一個(gè)發(fā)布者類的事件, 你不會(huì)希望發(fā)布者與所有這些類相耦合的。 此外如果他人會(huì)使用發(fā)布者類, 那么你甚至可能會(huì)對其中的一些類一無所知。
因此, 所有訂閱者都必須實(shí)現(xiàn)同樣的接口, 發(fā)布者僅通過該接口與訂閱者交互。 接口中必須聲明通知方法及其參數(shù), 這樣發(fā)布者在發(fā)出通知時(shí)還能傳遞一些上下文數(shù)據(jù)。
發(fā)布者調(diào)用訂閱者對象中的特定通知方法來通知訂閱者。
如果你的應(yīng)用中有多個(gè)不同類型的發(fā)布者, 且希望訂閱者可兼容所有發(fā)布者, 那么你甚至可以進(jìn)一步讓所有發(fā)布者遵循同樣的接口。 該接口僅需描述幾個(gè)訂閱方法即可。 這樣訂閱者就能在不與具體發(fā)布者類耦合的情況下通過接口觀察發(fā)布者的狀態(tài)。
真實(shí)世界類比
雜志和報(bào)紙訂閱。
如果你訂閱了一份雜志或報(bào)紙, 那就不需要再去報(bào)攤查詢新出版的刊物了。 出版社 (即應(yīng)用中的 “發(fā)布者”) 會(huì)在刊物出版后 (甚至提前) 直接將最新一期寄送至你的郵箱中。
出版社負(fù)責(zé)維護(hù)訂閱者列表, 了解訂閱者對哪些刊物感興趣。 當(dāng)訂閱者希望出版社停止寄送新一期的雜志時(shí), 他們可隨時(shí)從該列表中退出。
觀察者模式結(jié)構(gòu)
- 發(fā)布者 (Publisher) 會(huì)向其他對象發(fā)送值得關(guān)注的事件。 事件會(huì)在發(fā)布者自身狀態(tài)改變或執(zhí)行特定行為后發(fā)生。 發(fā)布者中包含一個(gè)允許新訂閱者加入和當(dāng)前訂閱者離開列表的訂閱構(gòu)架。
- 當(dāng)新事件發(fā)生時(shí), 發(fā)送者會(huì)遍歷訂閱列表并調(diào)用每個(gè)訂閱者對象的通知方法。 該方法是在訂閱者接口中聲明的。
- 訂閱者 (Subscriber) 接口聲明了通知接口。 在絕大多數(shù)情況下, 該接口僅包含一個(gè)
update
更新方法。 該方法可以擁有多個(gè)參數(shù), 使發(fā)布者能在更新時(shí)傳遞事件的詳細(xì)信息。 - 具體訂閱者 (Concrete Subscribers) 可以執(zhí)行一些操作來回應(yīng)發(fā)布者的通知。 所有具體訂閱者類都實(shí)現(xiàn)了同樣的接口, 因此發(fā)布者不需要與具體類相耦合。
- 訂閱者通常需要一些上下文信息來正確地處理更新。 因此, 發(fā)布者通常會(huì)將一些上下文數(shù)據(jù)作為通知方法的參數(shù)進(jìn)行傳遞。 發(fā)布者也可將自身作為參數(shù)進(jìn)行傳遞, 使訂閱者直接獲取所需的數(shù)據(jù)。
- 客戶端 (Client) 會(huì)分別創(chuàng)建發(fā)布者和訂閱者對象, 然后為訂閱者注冊發(fā)布者更新。
偽代碼
在本例中, 觀察者模式允許文本編輯器對象將自身的狀態(tài)改變通知給其他服務(wù)對象。
將對象中發(fā)生的事件通知給其他對象。
訂閱者列表是動(dòng)態(tài)生成的: 對象可在運(yùn)行時(shí)根據(jù)程序需要開始或停止監(jiān)聽通知。
在本實(shí)現(xiàn)中, 編輯器類自身并不維護(hù)訂閱列表。 它將工作委派給專門從事此工作的一個(gè)特殊幫手對象。 你還可將該對象升級為中心化的事件分發(fā)器, 允許任何對象成為發(fā)布者。
只要發(fā)布者通過同樣的接口與所有訂閱者進(jìn)行交互, 那么在程序中新增訂閱者時(shí)就無需修改已有發(fā)布者類的代碼。
觀察者模式適合應(yīng)用場景
當(dāng)一個(gè)對象狀態(tài)的改變需要改變其他對象, 或?qū)嶋H對象是事先未知的或動(dòng)態(tài)變化的時(shí), 可使用觀察者模式。
當(dāng)你使用圖形用戶界面類時(shí)通常會(huì)遇到一個(gè)問題。 比如, 你創(chuàng)建了自定義按鈕類并允許客戶端在按鈕中注入自定義代碼, 這樣當(dāng)用戶按下按鈕時(shí)就會(huì)觸發(fā)這些代碼。
觀察者模式允許任何實(shí)現(xiàn)了訂閱者接口的對象訂閱發(fā)布者對象的事件通知。 你可在按鈕中添加訂閱機(jī)制, 允許客戶端通過自定義訂閱類注入自定義代碼。
當(dāng)應(yīng)用中的一些對象必須觀察其他對象時(shí), 可使用該模式。 但僅能在有限時(shí)間內(nèi)或特定情況下使用。
訂閱列表是動(dòng)態(tài)的, 因此訂閱者可隨時(shí)加入或離開該列表。
實(shí)現(xiàn)方式
仔細(xì)檢查你的業(yè)務(wù)邏輯, 試著將其拆分為兩個(gè)部分: 獨(dú)立于其他代碼的核心功能將作為發(fā)布者; 其他代碼則將轉(zhuǎn)化為一組訂閱類。
聲明訂閱者接口。 該接口至少應(yīng)聲明一個(gè)
update
方法。聲明發(fā)布者接口并定義一些接口來在列表中添加和刪除訂閱對象。 記住發(fā)布者必須僅通過訂閱者接口與它們進(jìn)行交互。
確定存放實(shí)際訂閱列表的位置并實(shí)現(xiàn)訂閱方法。 通常所有類型的發(fā)布者代碼看上去都一樣, 因此將列表放置在直接擴(kuò)展自發(fā)布者接口的抽象類中是顯而易見的。 具體發(fā)布者會(huì)擴(kuò)展該類從而繼承所有的訂閱行為。
但是, 如果你需要在現(xiàn)有的類層次結(jié)構(gòu)中應(yīng)用該模式, 則可以考慮使用組合的方式: 將訂閱邏輯放入一個(gè)獨(dú)立的對象, 然后讓所有實(shí)際訂閱者使用該對象。
創(chuàng)建具體發(fā)布者類。 每次發(fā)布者發(fā)生了重要事件時(shí)都必須通知所有的訂閱者。
在具體訂閱者類中實(shí)現(xiàn)通知更新的方法。 絕大部分訂閱者需要一些與事件相關(guān)的上下文數(shù)據(jù)。 這些數(shù)據(jù)可作為通知方法的參數(shù)來傳遞。
但還有另一種選擇。 訂閱者接收到通知后直接從通知中獲取所有數(shù)據(jù)。 在這種情況下, 發(fā)布者必須通過更新方法將自身傳遞出去。 另一種不太靈活的方式是通過構(gòu)造函數(shù)將發(fā)布者與訂閱者永久性地連接起來。
客戶端必須生成所需的全部訂閱者, 并在相應(yīng)的發(fā)布者處完成注冊工作。
觀察者模式優(yōu)缺點(diǎn)
開閉原則。 你無需修改發(fā)布者代碼就能引入新的訂閱者類 (如果是發(fā)布者接口則可輕松引入發(fā)布者類)。
你可以在運(yùn)行時(shí)建立對象之間的聯(lián)系。
訂閱者的通知順序是隨機(jī)的。
與其他模式的關(guān)系
責(zé)任鏈模式
、命令模式
、中介者模式
和觀察者模式
用于處理請求發(fā)送者和接收者之間的不同連接方式:- 責(zé)任鏈按照順序?qū)⒄埱髣?dòng)態(tài)傳遞給一系列的潛在接收者, 直至其中一名接收者對請求進(jìn)行處理。
- 命令在發(fā)送者和請求者之間建立單向連接。
- 中介者清除了發(fā)送者和請求者之間的直接連接, 強(qiáng)制它們通過一個(gè)中介對象進(jìn)行間接溝通。
- 觀察者允許接收者動(dòng)態(tài)地訂閱或取消接收請求。
中介者和觀察者 之間的區(qū)別往往很難記住。 在大部分情況下, 你可以使用其中一種模式, 而有時(shí)可以同時(shí)使用。 讓我們來看看如何做到這一點(diǎn)。
中介者的主要目標(biāo)是消除一系列系統(tǒng)組件之間的相互依賴。 這些組件將依賴于同一個(gè)中介者對象。 觀察者的目標(biāo)是在對象之間建立動(dòng)態(tài)的單向連接, 使得部分對象可作為其他對象的附屬發(fā)揮作用。
有一種流行的中介者模式實(shí)現(xiàn)方式依賴于觀察者。 中介者對象擔(dān)當(dāng)發(fā)布者的角色, 其他組件則作為訂閱者, 可以訂閱中介者的事件或取消訂閱。 當(dāng)中介者以這種方式實(shí)現(xiàn)時(shí), 它可能看上去與觀察者非常相似。
當(dāng)你感到疑惑時(shí), 記住可以采用其他方式來實(shí)現(xiàn)中介者。 例如, 你可永久性地將所有組件鏈接到同一個(gè)中介者對象。 這種實(shí)現(xiàn)方式和觀察者并不相同, 但這仍是一種中介者模式。
假設(shè)有一個(gè)程序, 其所有的組件都變成了發(fā)布者, 它們之間可以相互建立動(dòng)態(tài)連接。 這樣程序中就沒有中心化的中介者對象, 而只有一些分布式的觀察者。
代碼示例
Go設(shè)計(jì)模式之觀察者模式講解和代碼示例_Golang_腳本之家 (jb51.net)
到此這篇關(guān)于Go設(shè)計(jì)模式之觀察者模式圖解的文章就介紹到這了,更多相關(guān)Go觀察者模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實(shí)現(xiàn)優(yōu)雅的將struct轉(zhuǎn)換為map
在項(xiàng)目實(shí)踐中,有時(shí)候我們需要將struct結(jié)構(gòu)體轉(zhuǎn)為map映射表,然后基于map做數(shù)據(jù)裁剪或操作。那么下面我來介紹下常用的兩種轉(zhuǎn)換方式,希望對大家有所幫助2023-01-01Golan中?new()?、?make()?和簡短聲明符的區(qū)別和使用
Go語言中的new()、make()和簡短聲明符的區(qū)別和使用,new()用于分配內(nèi)存并返回指針,make()用于初始化切片、映射和通道,并返回初始化后的對象,簡短聲明符:=可以簡化變量聲明和初始化過程,感興趣的朋友一起看看吧2025-01-01詳解prometheus監(jiān)控golang服務(wù)實(shí)踐記錄
這篇文章主要介紹了詳解prometheus監(jiān)控golang服務(wù)實(shí)踐記錄,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11利用go-zero在Go中快速實(shí)現(xiàn)JWT認(rèn)證的步驟詳解
這篇文章主要介紹了如何利用go-zero在Go中快速實(shí)現(xiàn)JWT認(rèn)證,本文分步驟通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-10-10Golang?中的?strconv?包常用函數(shù)及用法詳解
strconv是Golang中一個(gè)非常常用的包,主要用于字符串和基本數(shù)據(jù)類型之間的相互轉(zhuǎn)換,這篇文章主要介紹了Golang中的strconv包,需要的朋友可以參考下2023-06-06go語言異常panic和恢復(fù)recover用法實(shí)例
這篇文章主要介紹了go語言異常panic和恢復(fù)recover用法,實(shí)例分析了異常panic和恢復(fù)recover使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03