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 來代替繼承;
-
通過可變參數模板和完美轉發來消除接口變化產生的影響。
下面以一個具體的場景舉例:
- 有一個數據中心,數據中心負責從其他地方獲取數據,并對數據進行加工處理
- 有一個圖表組件,需要從數據中心拿數據進行可視化展示
- 有一個文本組件,需要從數據中心拿數據進行可視化展示
- 后期可能還有更多的組件,需要從數據中心拿數據…
/*** @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)、實時數據監控系統、消息中間件等項目時,觀察者模式能夠幫助你更好地組織代碼結構,提高系統的靈活性和可維護性。
一句話總結(發布-訂閱模式):
觀察者模式就像一個新聞通訊社,一旦有新的新聞發布,所有訂閱了該新聞的人都會立即收到通知。