趣谈设计模式 | 观察者模式(Observer) :消息的发布与订阅
文章目錄
- 案例:文章推送
- 觀察者模式
- 觀察者模式的運(yùn)作流程
- 觀察者模式解決的問題
- 觀察者模式大顯身手
- 總結(jié)
- 要點(diǎn)
- 應(yīng)用場(chǎng)景
- 生產(chǎn)者-消費(fèi)者模型 VS 觀察者模式
- 完整代碼及文檔
案例:文章推送
假設(shè)我是一個(gè)科幻小說愛好者,我維護(hù)著一個(gè)叫做ScienceFictionPusher的公眾號(hào),定期向豆瓣、知乎等平臺(tái)推送那些我覺得有趣的科幻小說,于是為了方便管理,我的推送程序是這樣的邏輯
上面這種實(shí)現(xiàn)方式咋一看沒什么問題,甚至在某些地方處理的還不錯(cuò),因?yàn)槲覀儗?nèi)容的更新從平臺(tái)主動(dòng)的拉取變?yōu)榱斯娞?hào)的主動(dòng)推送,大大減少了空轉(zhuǎn)時(shí)間。因此,我們將代碼投入使用
隨著粉絲越來越多,公眾號(hào)的名氣也越來越大,于是乎越來越多的平臺(tái)開始邀請(qǐng)我的專欄入駐,但是此時(shí)就出現(xiàn)了問題
如果采用上面這種模式的話,當(dāng)有大量的平臺(tái)時(shí),代碼會(huì)是這樣的,存在大量的冗余,可讀性也極差
由于公眾號(hào)的經(jīng)營(yíng)也存在波動(dòng),當(dāng)流量大的時(shí)候我們會(huì)有新增的平臺(tái),當(dāng)某個(gè)平臺(tái)流量小的時(shí)候我們也不會(huì)再去維護(hù),所以平臺(tái)的數(shù)量是時(shí)刻變化的,那這樣的代碼就意味著我們需要時(shí)刻去程序中修改,無法動(dòng)態(tài)的增加、刪除,效率極低。
那有什么好的解決方法嗎?這就到了 觀察者模式出場(chǎng)的時(shí)候
觀察者模式
觀察者模式也叫做發(fā)布訂閱模式,它定義了對(duì)象之間的一對(duì)多依賴,當(dāng)一個(gè)對(duì)象改變狀態(tài)的時(shí)候,它的所有依賴著都會(huì)收到通知并自動(dòng)更新。
為了方便舉例,這里我們將發(fā)布內(nèi)容的對(duì)象稱為主題,接收內(nèi)容的對(duì)象稱為觀察者
觀察者模式的運(yùn)作流程
此時(shí)對(duì)象C也想要獲取內(nèi)容,所以它告訴主題他想要注冊(cè)成為觀察者
由于主題發(fā)布的內(nèi)容質(zhì)量逐漸降低,對(duì)象A不再需要訂閱,此時(shí)它請(qǐng)求注銷主題
從上面我們可以看到,主題主要做了三件事,注冊(cè)、刪除、通知觀察者。而觀察者所做的只是被動(dòng)的接受主題提供的數(shù)據(jù)
觀察者模式解決的問題
講了這么多,其實(shí)觀察者模式最主要的作用就是讓主題和觀察者松耦合:即這兩個(gè)對(duì)象雖然互相可以交互,但是它們都不清楚彼此的細(xì)節(jié)
主題只知道觀察者實(shí)現(xiàn)了Observer接口,它并不需要知道觀察者的具體類是誰(shuí),也不需要了解它究竟實(shí)現(xiàn)了什么,它只需要調(diào)用觀察者的update將數(shù)據(jù)更新過去即可。
同樣的,因?yàn)橹黝}依賴的只是實(shí)現(xiàn)了Observer接口的對(duì)象列表,所以無論我們是對(duì)觀察者增加還是刪除,都不會(huì)對(duì)主題造成影響,主題也不需要為了兼容這些觀察者而去修改代碼。
甚至我們還可以在其他地方獨(dú)立的復(fù)用主題和觀察者,例如我們新增一個(gè)新的主題,又或者是新增一個(gè)觀察者,由于二者并非緊耦合,所以不會(huì)有任何的影響。
總結(jié)一下就是,這種設(shè)計(jì)將對(duì)象之間的互相依賴降到了最低,因此我們的程序具有彈性,能夠應(yīng)對(duì)各種變化。
觀察者模式大顯身手
回到上面的問題,當(dāng)我們的公眾號(hào)發(fā)布新內(nèi)容的時(shí)候,我們會(huì)將這些內(nèi)容推送到所有的入駐平臺(tái)中,這正好就符合上面所說的觀察者模式的場(chǎng)景。此時(shí)公眾號(hào)充當(dāng)主題對(duì)象,而平臺(tái)充當(dāng)觀察者。
此時(shí)完整的關(guān)系圖如下
根據(jù)上面所提到的內(nèi)容,我們抽象出具體的主題接口和觀察者接口。為了方便使用不同語(yǔ)言的讀者閱讀,我會(huì)盡量少用C++的特性,如果還是有不理解的可以私信或者評(píng)論區(qū)留言。
主題接口只需要提供必須的注冊(cè)、刪除、發(fā)布即可
class Subject { public:virtual ~Subject() = default;virtual void registerObserver(Observer*) = 0; //注冊(cè)觀察者virtual void removeObserver(Observer*) = 0; //移除觀察者virtual void notifyObservers() = 0; //通知所有觀察者 };觀察者被動(dòng)等待主題的數(shù)據(jù),所以我們也只提供一個(gè)更新接口供主題更新數(shù)據(jù)
class Observer { public:virtual ~Observer() = default;virtual void update(const std::string& url, const std::string& title, const std::string& desc) = 0; //更新數(shù)據(jù) };考慮到每個(gè)平臺(tái)獲取到新內(nèi)容都必定要將其展示出來,而每個(gè)平臺(tái)展示的方式又有所不同,所以我們將其再抽象為一個(gè)接口類,觀察者需要繼承這個(gè)類并實(shí)現(xiàn)自己的展示方法
class DisplayElement { public:virtual ~DisplayElement() = default;virtual void display() = 0; //顯示數(shù)據(jù) };下面就開始具體實(shí)例的實(shí)現(xiàn)吧
為了保證不會(huì)對(duì)同一平臺(tái)重復(fù)發(fā)送,以及后續(xù)可能會(huì)對(duì)某些平臺(tái)單獨(dú)推送內(nèi)容,我們使用一個(gè)哈希表來存儲(chǔ)所有入駐的平臺(tái)
//主題派生子類 class ScienceFictionPusher : public Subject { public://增加觀察者void registerObserver(Observer* observer){_observers.insert(observer);}//刪除觀察者void removeObserver(Observer* observer){_observers.erase(observer);}//向所有平臺(tái)推送內(nèi)容void notifyObservers(){for(const auto& ob : _observers){ob->update(_url, _title, _desc);}}//推送新內(nèi)容void newPush(){notifyObservers();}//設(shè)置新內(nèi)容,當(dāng)有新內(nèi)容發(fā)布的時(shí)候,就會(huì)自動(dòng)推送給所有的平臺(tái)void setNewFiction(const std::string& url, const std::string& title, const std::string& desc){_url = url;_title = title;_desc = desc;newPush();}private:std::string _url; //小說鏈接std::string _title; //小說名std::string _desc; //小說簡(jiǎn)介std::unordered_set<Observer*> _observers; //入駐的平臺(tái) };當(dāng)有新的平臺(tái)想要入駐的時(shí)候,它只需要繼承觀察者類并實(shí)現(xiàn)update接口即可,同時(shí)由于我們接收新內(nèi)容后還需要在自身平臺(tái)中顯示,所以還需要繼承發(fā)布內(nèi)容類,并實(shí)現(xiàn)display接口
為了方便注冊(cè)和刪除觀察者,我們需要保存一個(gè)指向主題的指針
//觀察者派生子類 class Zhihu : public Observer, public DisplayElement { public:Zhihu(Subject* ScienceFictionPusher): _ScienceFictionPusher(ScienceFictionPusher){_ScienceFictionPusher->registerObserver(this);}~Zhihu(){_ScienceFictionPusher->removeObserver(this);}//實(shí)現(xiàn)更新接口,讓主題主動(dòng)推送數(shù)據(jù)void update(const std::string& url, const std::string& title, const std::string& desc){_url = url;_title = title;_desc = desc;display();}//在平臺(tái)中顯示推送的內(nèi)容void display(){std::cout << "知乎每日書籍推薦:" << std::endl;std::cout << "鏈接:" << _url << std::endl;std::cout << "標(biāo)題:" << _title << std::endl;std::cout << "簡(jiǎn)介:" << _desc << "\n" <<std::endl; }private:std::string _url; //小說鏈接std::string _title; //小說名std::string _desc; //小說簡(jiǎn)介Subject* _ScienceFictionPusher; //主題對(duì)象,方便注冊(cè)和刪除 };其他的觀察者也類似,為了節(jié)省篇幅這里就不多寫了,下面寫個(gè)簡(jiǎn)單的程序測(cè)試一下
int main() {ScienceFictionPusher* _subject = new ScienceFictionPusher;Douban* douban = new Douban(_subject);Zhihu* zhihu = new Zhihu(_subject);_subject->setNewFiction("www.aaaaaaa.com", "三體", "作品講述了地球人類文明和三體文明的信息交流、生死搏殺及兩個(gè)文明在宇宙中的興衰歷程。");_subject->setNewFiction("www.bbbbbbb.com", "球形閃電", "描述了一個(gè)歷經(jīng)球狀閃電的男主角對(duì)其歷盡艱辛的研究歷程,向我們展現(xiàn)了一個(gè)獨(dú)特、神秘而離奇的世界");delete zhihu;delete douban;delete _subject;return 0; }我們添加了知乎和豆瓣兩個(gè)觀察者,并且連續(xù)推送了三體和球形閃電這兩條內(nèi)容
可以看到,測(cè)試結(jié)果沒有問題
總結(jié)
要點(diǎn)
- 觀察者模式定義了對(duì)象之間一對(duì)多的關(guān)系
- 觀察者模式使得我們可以獨(dú)立地改變主題與觀察者,從而使二者之間的依賴關(guān)系達(dá)致松耦合。
- 主題發(fā)送通知時(shí),需要遍歷觀察者,因此其知道觀察者的存在
- 觀察者自己決定是否需要訂閱通知,主題對(duì)象對(duì)此一無所知。
應(yīng)用場(chǎng)景
觀察者模式應(yīng)該可以說是應(yīng)用最多、影響最廣的模式之一,它通常應(yīng)用于游戲引擎、GUI、郵件訂閱等場(chǎng)景
場(chǎng)景1 :游戲中的事件監(jiān)控
例如我們?cè)O(shè)計(jì)了一個(gè)RPG游戲,當(dāng)我們的角色移動(dòng)到敵人的視野范圍時(shí),周圍的敵人就會(huì)向角色移動(dòng)并且發(fā)起攻擊。當(dāng)我們移動(dòng)到陷阱的觸發(fā)位置時(shí),陷阱就會(huì)對(duì)我們?cè)斐蓚Α.?dāng)我們移動(dòng)到泉水時(shí),泉水又會(huì)為角色提供治療或者BUFF。
在上面的例子中,我們的角色就是一個(gè)主題,而泉水、陷阱、敵人這些就是觀察者,當(dāng)我們做出了某種舉動(dòng)的時(shí)候,就會(huì)通知它們這些事件的發(fā)生,它們就會(huì)做出一個(gè)具體的響應(yīng)。這樣就能夠保證事件實(shí)時(shí)的同步,以及方便我們進(jìn)行拓展,后續(xù)向增加新事件例如減速的泥潭等內(nèi)容只需要將其注冊(cè)為觀察者并實(shí)現(xiàn)邏輯即可。
場(chǎng)景2:GUI界面的事件偵聽
在GUI界面中,通常有著許多的選項(xiàng), 而在這些選項(xiàng)背后,通常又有多個(gè)負(fù)責(zé)不同功能的偵聽者等待我們的結(jié)果,當(dāng)我們按下這個(gè)按鈕的時(shí)候,就會(huì)通知負(fù)責(zé)這一功能的一系列偵聽者響應(yīng)號(hào)召,執(zhí)行它們各自的工作,這也是一種觀察者模式
生產(chǎn)者-消費(fèi)者模型 VS 觀察者模式
說到數(shù)據(jù)的生產(chǎn)和發(fā)布、解耦合這兩方面,那就難免要提到生產(chǎn)者消費(fèi)者模型,下面給出它們兩個(gè)的對(duì)比圖。
如果不了解生產(chǎn)者消費(fèi)者模型的可以參考我的往期博客
操作系統(tǒng):生產(chǎn)者消費(fèi)者模型的兩種實(shí)現(xiàn)(C++)
相同點(diǎn)
- 主要作用都是解耦合
- 兩者都是行為模式,本質(zhì)上都是發(fā)布-消費(fèi)兩個(gè)行為
不同點(diǎn)
- 觀察者模式是一對(duì)多,一條消息可以被多個(gè)觀察者使用
- 生產(chǎn)者-消費(fèi)者模型是多對(duì)多的,并且一條消息只能被一個(gè)消費(fèi)者使用
- 觀察者模式可以同步實(shí)現(xiàn),也可以異步實(shí)現(xiàn)
- 生產(chǎn)者消費(fèi)者模式依賴于交易場(chǎng)所,只能異步實(shí)現(xiàn)
- 觀察者模式中主題知道觀察者的存在,因?yàn)樗枰闅v訂閱列表發(fā)送通知,因此兩者之間還是存在微弱的耦合關(guān)系
- 生產(chǎn)者和消費(fèi)者借助交易場(chǎng)所(中間隊(duì)列),它們只需要往隊(duì)列中生成/消費(fèi)數(shù)據(jù),因此不需要知道對(duì)方的存在,屬于完全解耦
完整代碼及文檔
如果有需要完整代碼或者markdown文檔的同學(xué)可以點(diǎn)擊下面的github鏈接
github
總結(jié)
以上是生活随笔為你收集整理的趣谈设计模式 | 观察者模式(Observer) :消息的发布与订阅的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Shell程序设计 | 文本处理工具 :
- 下一篇: 趣谈设计模式 | 工厂模式(Factor