【C++設計模式】第三篇:觀察者模式(別名:發布-訂閱模式、模型-視圖模式、源-監聽器模式)

C++設計模式系列文章目錄

【C++設計模式】第一篇 C++單例模式–懶漢與餓漢以及線程安全

【C++設計模式】第二篇:策略模式(Strategy)–從基本介紹,內部原理、應用場景、使用方法,常見問題和解決方案進行深度解析


【C++設計模式】第三篇:觀察者模式(別名:發布-訂閱模式、模型-視圖模式、源-監聽器模式)

  • C++設計模式系列文章目錄
  • 一、觀察者模式簡介
  • 二、解決的問題類型
  • 三、使用場景
  • 四、核心概念
  • 五、實現方式
    • 5.1 基礎方式
    • 5.2 改進觀察者模式
    • 5.3 總結
  • 八、最終小結
    • 一句話總結(發布-訂閱模式):


原文鏈接:https://blog.csdn.net/weixin_44131922/article/details/149515925

一、觀察者模式簡介

觀察者模式(Observer Pattern) 是一種 行為型設計模式(對象行為型模式),它定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。當主題對象發生變化時,它的所有依賴者(觀察者)都會收到通知并自動更新。

你可以把它想象成“訂閱-發布”機制:當你訂閱了一份雜志后,每當有新一期出版時,你就會收到通知。這里的“你”就是觀察者,“雜志”則是被觀察的主題。

在這里插入圖片描述

“紅燈停,綠燈行”。在這個過程中,交通信號燈是汽車的觀察目標,而汽車則是觀察者。隨著交通信號燈的變化,汽車的行為也隨之變化,一盞交通信號燈可以指揮多輛汽車。

在軟件系統中有些對象之間也存在類似交通信號燈和汽車之間的關系,一個對象的狀態或行為的變化將導致其他對象的狀態或行為也發生改變。觀察者模式(Observer Pattern)則是用于建立一種對象與對象之間的依賴關系,使得每當一個對象狀態發生改變時其相關依賴對象皆得到通知并被自動更新。發生改變的對象稱為觀察目標,被通知的對象稱為觀察者,一個觀察目標可以對應多個觀察者。它有如下別名:

  • 發布-訂閱(Publish/Subscribe)模式
  • 模型-視圖(Model/View)模式
  • 源-監聽器(Source/Listener)模式
  • 從屬者(Dependents)模式

二、解決的問題類型

觀察者模式主要用于解決以下問題:

  • 狀態變化需要通知相關聯的對象:例如,用戶界面中的模型數據發生變化時,視圖需要自動更新。需要在系統中創建一個觸發鏈。
  • 避免緊耦合:允許對象之間保持松散的聯系,無需直接相互引用。一個抽象模型有兩個方面,其中一個方面依賴于另一個方面,將這兩個方面封裝在獨立的對象中使它們可以各自獨立地改變和復用。
  • 實現廣播通信:可以向所有注冊的觀察者發送消息,而不需要知道它們的具體身份。

三、使用場景

在這里插入圖片描述

四、核心概念

Subject(主題/被觀察者):維護了一個觀察者列表,并提供方法供觀察者注冊或移除自身;在狀態改變時通知所有觀察者。
Observer(觀察者):接收來自主題的通知并作出響應。
ConcreteSubject & ConcreteObserver:具體實現上述兩個接口的實際類。

五、實現方式

原文鏈接:https://blog.csdn.net/hzdxyh/article/details/141126923

  • Observer(觀察者):它是一個抽象類或接口,為所有的具體觀察者定義一個更新接口,使得在得到主題的通知時更新自己。
  • Subject(主題):它維護了一系列依賴于它的Observer對象,并提供一個接口來允許Observer對象注冊自己、注銷自己以及通知它們。
  • ConcreteObserver(具體觀察者):它實現了Observer接口,存儲與Subject的狀態自洽的狀態。具體觀察者根據需要實現Subject的更新接口,以使得自身狀態與主題的狀態保持一致。
  • ConcreteSubject(具體主題):它實現了Subject接口,將有關狀態存入具體觀察者對象,并在狀態發生改變時向Observer發出通知。
    在這里插入圖片描述

5.1 基礎方式

按定義實現最基礎的觀察者模式功能:

/*** @brief 定義一個Observer抽象基類*/
class IObserver {public:IObserver() {}virtual ~IObserver() {}public:virtual void Update(int data) = 0;
};/*** @brief 定義一個Subject抽象基類*/
class ISubject {public:ISubject() {}virtual ~ISubject() {}public:virtual void Subscribe(std::shared_ptr<IObserver> observer) = 0;  // 觀察者訂閱事件virtual void Unsubscribe(std::shared_ptr<IObserver> observer) = 0;  // 觀察者取消事件的訂閱virtual void Notify(int data) = 0;  // 通知已訂閱指定事件的觀察者public:std::list<std::weak_ptr<IObserver>> observers_;  // 存放所有已訂閱的observer
};/*** @brief 實現一個具體的觀察者*/
class ConcreteObserver : public IObserver {public:ConcreteObserver(const std::string& name) : name_(name) {}virtual ~ConcreteObserver() override {}public:void Update(int data) override {std::cout << "observer [" << name_ << "] updated -> " << data << std::endl;}private:std::string name_;
};/*** @brief 實現一個具體的Subject*/
class ConcreteSubject : public ISubject {public:ConcreteSubject() {}virtual ~ConcreteSubject() override {}public:void Subscribe(std::shared_ptr<IObserver> observer) override {observers_.push_back(observer);};void Unsubscribe(std::shared_ptr<IObserver> observer) override {observers_.erase(std::remove_if(observers_.begin(), observers_.end(),[&observer](std::weak_ptr<IObserver> obj) {std::shared_ptr<IObserver> tmp =obj.lock();if (tmp != nullptr) {return tmp == observer;} else {return false;}}),observers_.end());}void Notify(int data) override {for (auto it = observers_.begin(); it != observers_.end(); ++it) {std::shared_ptr<IObserver> ps = it->lock();// weak_ptr提升為shared_ptr// 判斷對象是否還存活if (ps != nullptr) {ps->Update(data);} else {it = observers_.erase(it);}}}
};// 測試
int main() {// 構造3個觀察者對象std::shared_ptr<IObserver> observer1(new ConcreteObserver("observer1"));std::shared_ptr<IObserver> observer2(new ConcreteObserver("observer2"));std::shared_ptr<IObserver> observer3(new ConcreteObserver("observer3"));// 構造1個主題對象ConcreteSubject subject;// 為觀察者訂閱事件subject.Subscribe(observer1);subject.Subscribe(observer2);subject.Subscribe(observer3);// 通知訂閱事件的觀察者subject.Notify(10);// 模擬取消訂閱subject.Unsubscribe(observer1);// 通知訂閱事件的觀察者subject.Notify(20);return 0;
}

控制臺輸出:

observer [observer1] updated -> 10
observer [observer2] updated -> 10
observer [observer3] updated -> 10
observer [observer2] updated -> 20
observer [observer3] updated -> 20

注意:在ISubject 中維護了一個已訂閱的observer的list,在list中存放了observer對象的指針,在很多例子中list中直接存放observer的裸指針,在notify通知所有observer時,需要遍歷list,調用每個observer的update接口,在多線程環境中,肯定不明確此時observer對象是否還存活,或是已經在其它線程中被析構了。本例這里使用了weak_ptr和shared_ptr替代observer裸指針,解決上述問題,同時還能避免循環引用

從上面的例子中可以看出:Observer是不依賴于Subject的,想要增加一個新的Observer只需要繼承IObserver即可,無需修改Subject,這符合開閉原則,也實現了Observer與Subject的解耦。

5.2 改進觀察者模式

上面例子中的觀察者模式是經典模式,但是存在缺陷:

  • 需要繼承,繼承是強對象關系,只能對特定的觀察者才有效,即必須是Observer抽象類的派生類才行;

  • 觀察者被通知的接口參數不支持變化,導致觀察者不能應付接口的變化
    為了解決上例觀察者模式的缺陷,可使用C++11 做出改進

  • 通過被通知接口參數化和std::function 來代替繼承;

  • 通過可變參數模板和完美轉發來消除接口變化產生的影響。

下面以一個具體的場景舉例:

  1. 有一個數據中心,數據中心負責從其他地方獲取數據,并對數據進行加工處理
  2. 有一個圖表組件,需要從數據中心拿數據進行可視化展示
  3. 有一個文本組件,需要從數據中心拿數據進行可視化展示
  4. 后期可能還有更多的組件,需要從數據中心拿數據…
/*** @brief 實現一個具體的Subject,模擬一個數據中心*/
template <typename Func>
class Subject {public:static Subject& GetInstance() {static Subject instance;return instance;}public:// 注冊觀察者,右值引用int Subscribe(Func&& f) { return Assign(f); }// 注冊觀察者,左值int Subscribe(const Func& f) { return Assign(f); }// 移除觀察者void Unsubscribe(int id) { observers_map_.erase(id); }// 通知所有觀察者template <typename... Args>void Notify(Args&&... args) {for (auto& it : observers_map_) {auto& func = it.second;func(std::forward<Args>(args)...);}}private:template <typename F>int Assign(F&& f) {int id = observer_id_++;observers_map_.emplace(id, std::forward<F>(f));return id;}private:Subject() = default;~Subject() = default;Subject(const Subject&) = delete;Subject& operator=(const Subject&) = delete;Subject(Subject&&) = delete;Subject& operator=(Subject&&) = delete;int observer_id_ = 0;                //觀察者對應編號std::map<int, Func> observers_map_;  // 觀察者列表
};/*** @brief 實現一個具體的觀察者,模擬圖表展示組件*/
class ChartView {public:ChartView() {}~ChartView() {}public:void Update(int data) {std::cout << "the chart data has been updated to [" << data << "]" << std::endl;}
};/*** @brief 實現一個具體的觀察者,模擬文本展示組件*/
class TextView {public:TextView() {}~TextView() {}public:void Update(std::string key, std::string value) {std::cout << "the text data has been updated to [" << key << ": " << value << "]" << std::endl;}
};// 測試程序
int main(void) {ChartView cv;  // 圖表展示組件// 從數據中心訂閱Subject<std::function<void(int)>>::GetInstance().Subscribe(std::bind(&ChartView::Update, cv, std::placeholders::_1));TextView tv;  // 文本展示組件// 從數據中心訂閱Subject<std::function<void(std::string, std::string)>>::GetInstance().Subscribe(std::bind(&TextView::Update, tv, std::placeholders::_1, std::placeholders::_2));// 這里模擬一個數據處理中心線程std::thread([]() {int cnt = 0;while (1) {// 模擬數據獲取數據處理過程std::this_thread::sleep_for(std::chrono::seconds(1));auto time = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();// 數據處理完成后,通知組件Subject<std::function<void(int)>>::GetInstance().Notify(cnt++);Subject<std::function<void(std::string, std::string)>>::GetInstance().Notify("time", std::to_string(time));}}).detach();// 主線程while (1) {std::cout << "main run ..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));}return 0;
}
main run ...
the chart data has been updated to [0]
the text data has been updated to [time: 1723446799]
the chart data has been updated to [1]
the text data has been updated to [time: 1723446800]
the chart data has been updated to [2]
the text data has been updated to [time: 1723446801]
the chart data has been updated to [3]
the text data has been updated to [time: 1723446802]
main run ...
the chart data has been updated to [4]
the text data has been updated to [time: 1723446803]

在本例中,將Subject改為單例模式,這樣在組件中調用注冊接口,在數據中心調用通知接口,完全解耦分離;圖表組件和文本組件所需的數據個數和數據類型是不同的,這在基礎的觀察者模式中是無法實現的,改進后的觀察者模式脫離了需要繼承的約束,可以實現更加通用的功能,后期擴展更多組件時,不需要修改Subject代碼,只需要新增Observer即可。

5.3 總結

觀察者模式的優點主要包括:
解耦:觀察者和被觀察的對象是抽象耦合的,即它們之間不直接調用,而是通過消息傳遞來通知。
靈活性:可以在運行時動態地添加或刪除觀察者。
復用性:觀察者模式可以單獨地重用主題和觀察者。
缺點
開銷:如果觀察者非常多,那么更新的效率就會比較低,因為需要遍歷所有的觀察者,并調用它們的更新方法。
在這里插入圖片描述

八、最終小結

觀察者模式是一種非常實用的設計模式,特別適合那些需要在不同組件之間維持松散耦合并且能夠響應狀態變化的應用場景。通過合理地運用觀察者模式,我們可以在不破壞原有模塊獨立性的前提下,有效地實現組件間的協作與通信。

在開發圖形用戶界面(GUI)、實時數據監控系統、消息中間件等項目時,觀察者模式能夠幫助你更好地組織代碼結構,提高系統的靈活性和可維護性。

一句話總結(發布-訂閱模式):

觀察者模式就像一個新聞通訊社,一旦有新的新聞發布,所有訂閱了該新聞的人都會立即收到通知。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/921715.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/921715.shtml
英文地址,請注明出處:http://en.pswp.cn/news/921715.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

運作管理學習筆記5-生產和服務設施的選址

運作管理-北京交通大學5.1.設施選址概述 設施選址是一個戰略性的決策&#xff0c;做這個決策的時候會投入比較多的資源&#xff0c;而且未來去改變選址的成本和代價也比較大。 5.1.1.設施選址的重要性 設施選址影響企業經營情況 設施選址對設施布局以及投產后的生產經營費用、產…

JUnit 詳解

一、JUnit 簡介&#xff1a;什么是 JUnit&#xff1f;為什么要用它&#xff1f;1.1 核心定義JUnit 是一個開源的、基于 Java 語言的單元測試框架&#xff0c;最初由 Erich Gamma (GoF 設計模式作者之一) 和 Kent Beck (極限編程創始人) 在 1997 年共同開發。作為 xUnit 測試框架…

數據結構造神計劃第三天---數據類型

&#x1f525;個人主頁&#xff1a;尋星探路 &#x1f3ac;作者簡介&#xff1a;Java研發方向學習者 &#x1f4d6;個人專欄&#xff1a;《從青銅到王者&#xff0c;就差這講數據結構&#xff01;&#xff01;&#xff01;》、 《JAVA&#xff08;SE&#xff09;----如此簡單&a…

AI API Tester體驗:API測試工具如何高效生成接口測試用例、覆蓋異常場景?

前陣子幫后端測試支付接口時&#xff0c;我算是徹底明白 “API 測試能磨掉半條命”—— 明明接口文檔里寫了十幾種參數組合&#xff0c;手動寫測試用例時要么漏了 “簽名過期” 的場景&#xff0c;要么忘了校驗 “金額超過限額” 的返回值&#xff0c;測到半夜還被開發吐槽 “你…

音頻驅動數字人人臉模型

1.LatentSync: Taming Audio-Conditioned Latent Diffusion Models for Lip Sync with SyncNet Supervision 字節 2024 文章地址&#xff1a;https://arxiv.org/pdf/2412.09262 代碼地址&#xff1a;https://github.com/bytedance/LatentSync 訓練推理都有 2.wan2.2-s2v …

CentOS部署ELK Stack完整指南

文章目錄&#x1f680; ELK Stack 部署詳解&#xff08;CentOS 7/8&#xff09;&#x1f4e6; 一、環境準備1. 關閉防火墻&#xff08;或開放端口&#xff09;2. 關閉 SELinux3. 安裝基礎依賴4. 驗證 Java&#x1f53d; 二、下載并安裝 ELK 組件1. 導入 Elastic GPG 密鑰2. 創建…

Spring Boot 攔截器(Interceptor)與過濾器(Filter)有什么區別?

在 Spring Boot 項目中&#xff0c;我們經常會遇到需要在請求處理前后執行一些通用邏輯的場景&#xff0c;比如記錄日志、權限校驗、全局異常處理等。此時&#xff0c;我們通常會面臨兩種選擇&#xff1a;過濾器&#xff08;Filter&#xff09; 和 攔截器&#xff08;Intercept…

【技術教程】如何將文檔編輯器集成至基于Java的Web應用程序

在如今的企業協作場景中&#xff0c;“文檔” 早已不是簡單的文字載體&#xff01;從項目需求文檔的多人實時修改&#xff0c;到財務報表的在線批注&#xff0c;再到合同草案的版本追溯&#xff0c;用戶越來越需要在 Web 應用內直接完成 “編輯 - 協作 - 存儲” 全流程。 但很…

多模態大模型Keye-VL-1.5發布!視頻理解能力更強!

近日&#xff0c;快手正式發布了多模態大語言模型Keye-VL-1.5-8B。 與之前的版本相比&#xff0c;Keye-VL-1.5的綜合性能實現顯著提升&#xff0c;尤其在基礎視覺理解能力方面&#xff0c;包括視覺元素識別、推理能力以及對時序信息的理—表現尤為突出。Keye-VL-1.5在同等規模…

洗完頭后根據個人需求選擇合適的自然風干 | 電吹風 (在保護發質的同時,也能兼顧到生活的便利和舒適。)

文章目錄 引言 I 選合適的方式讓頭發變干 時間充裕,不需要做造型,選擇自然風干 使用電吹風,比較推薦的做法 II 自然風干 天冷可能刺激頭皮 III 電吹風吹干 容易造型 影響頭皮健康 損傷發質 科普 頭皮的微觀結構 頭發絲 引言 吹風吹干:容易造型,但損傷發質、影響頭皮健康 …

GPS汽車限速器有哪些功能?主要運用在哪里?

GPS 汽車限速器是一種結合全球衛星定位&#xff08;GPS&#xff09;技術、車速采集技術與車輛控制 / 預警邏輯的設備&#xff0c;核心目標是通過技術手段限制車輛行駛速度&#xff0c;減少超速引發的交通事故&#xff0c;并輔助車輛管理。其功能與應用場景高度匹配不同用戶的 “…

Python從入門到精通_01_python基礎

1 源代碼格式在python文件的第一行&#xff0c;輸入以下語句&#xff0c;可以將python文件的編碼格式設置為utf-8#-*- coding:utf-8 -*-2 輸入輸出input():輸入&#xff0c;無論輸入的是什么類型數據&#xff0c;最后都是字符串類型print(*args, sep , end\n, fileNone, flushF…

使用CI/CD部署項目(前端Nextjs)

寫在前面&#xff1a;在github上使用CI/CD部署Nextjs項目&#xff0c;具體配置可以按照自己的實際的修改 這是我的項目配置&#xff0c;僅供參考 后端項目可以參考&#xff1a;使用CI/CD部署后端項目 正文開始 項目名&#xff08;PROJECT_NAME&#xff09;- CI/CD 部署指南…

Java全棧工程師面試實錄:從基礎到實戰的全面解析

Java全棧工程師面試實錄&#xff1a;從基礎到實戰的全面解析 面試官&#xff1a;李明&#xff08;資深技術負責人&#xff09; 應聘者&#xff1a;張宇&#xff08;28歲&#xff0c;碩士學歷&#xff0c;5年開發經驗&#xff09; 第一輪&#xff1a;Java語言與JVM基礎 李明&…

C#中解析XML時遇到注釋節點報錯

在C#中解析XML時遇到注釋節點報錯的問題&#xff0c;這是因為XML注釋節點&#xff08;<!-- -->&#xff09;是特殊的節點類型。當遍歷XML節點時&#xff0c;注釋節點也會被包含在內&#xff0c;但它們不能像普通元素節點那樣處理。 解決方案 方法1&#xff1a;跳過注釋節…

9.3深度循環神經網絡

目前為止&#xff0c;只討論了具有一個單向隱藏層的循環神經網絡&#xff0c;其中隱變量和觀測值域具體的函數形式的交互方式是相當隨意的。只要交互類型建模具有足夠的靈活性&#xff0c;不是一個單問題。然而&#xff0c;對一個單層來說&#xff0c;可能具有相當的挑戰性。之…

CSS in JS 的演進:Styled Components, Emotion 等的深度對比與技術選型指引

CSS in JS 的演進&#xff1a;Styled Components, Emotion 等的深度對比與技術選型指引在現代前端開發中&#xff0c;組件化思維已成為主流&#xff0c;而如何科學、高效地管理組件的樣式&#xff0c;也隨之成為了一個重要議題。CSS in JS&#xff08;JS中的CSS&#xff09;應運…

【正則表達式】 正則表達式的分組和引用

?? 個人主頁:(時光煮雨) ?? 高質量專欄:vulnhub靶機滲透測試 ?? 希望得到您的訂閱和支持~ ?? 創作高質量博文(平均質量分95+),分享更多關于網絡安全、Python領域的優質內容!(希望得到您的關注~) ??目錄?? 前言 ??一、基本語法 ??二、分組類型 ??2.1.…

Grafana 導入儀表盤失敗:從日志排查到解決 max\_allowed\_packet 問題

問題背景 近期在為項目搭建一套基于 Prometheus 和 Grafana 的可觀測性體系。在完成基礎部署后&#xff0c;我準備導入一個功能相對復雜的官方儀表盤模板&#xff0c;以便快速監控各項指標。然而&#xff0c;當上傳儀表盤的 JSON 文件并點擊保存時&#xff0c;Grafana 界面卻反…

java對接物聯網設備(一)——使用okhttp網絡工具框架對接標準API接口

當前無論是在互聯網領域&#xff0c;還是物聯網項目下&#xff0c;亦或者各類應用類軟件&#xff0c;基于http標準接口的對接是目前市面上最常見也是最簡單的數據交互方式之一&#xff0c;甚至可以說是最流行的&#xff0c;因為它不依賴的各種插件或者服務。 開發者或者提供服…