文章目錄
- 1.觀察者模式簡介
- 2.觀察者模式結構
- 3.觀察者模式代碼實例
- 3.0.公共頭文件
- 3.1.觀察者
- 3.1.1.抽象觀察者Observer
- 3.1.2.具體觀察者Player
- 3.2.目標類
- 3.2.1.抽象目標AllyCenter
- 3.2.2.具體目標AllyCenterController
- 循環包含
- 錯誤示例
- “前向聲明什么時候不夠、必須 #include 對方頭?”
- 3.3.客戶端代碼示例及效果
- 4.觀察者模式的應用
- 5.總結
代碼倉庫
觀察者模式非常常見,近年來逐漸流行的響應式編程就是觀察者模式的應用之一。觀察者模式的思想就是一個對象發生一個事件后,逐一通知監聽著這個對象的監聽者,監聽者可以對這個事件馬上做出響應。
生活中有很多觀察者模式的例子,比如我們平時的開關燈。當我們打開燈的開關時,燈馬上亮了;當我們關閉燈的開關時,燈馬上熄了。這個過程中,燈就對我們控制開關的事件做出了響應,這就是一個最簡單的一對一觀察者模式。當某某公眾號發表一篇文章,所有關注了公眾號的讀者立即收到了文章,這個過程中所有關注了公眾號的微信客戶端就對公眾號發表文章的事件做出了響應,這就是一個典型的一對多觀察者模式。 這表明“更新發布文章”并不是孤立的,而是與眾多對象產生了關聯。一個對象行為的改變,其相關聯的對象都會得到通知,并自動產生對應的行為。這在軟件設計模式中,即是觀察者模式。
再舉個例子,比如警察一直觀察著張三的一舉一動,只要張三有什么違法行為,警察馬上行動,抓捕張三。
眾所周知,張三壞事做盡,是一個老法外狂徒了,所以不止一個警察會盯著張三,也就是說一個被觀察者可以有多個觀察者。當被觀察者有事件發生時,所有觀察者都能收到通知并響應。觀察者模式主要處理的是一種一對多的依賴關系。 它的定義如下:
觀察者模式(Observer Pattern):定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都得到通知并被自動更新。
1.觀察者模式簡介
軟件系統中的對象并不是孤立存在的,一個對象行為的改變可能會引起其他所關聯的對象的狀態或行為也發生改變,即“牽一發而動全身”。觀察者模式建立了一種一對多的聯動,一個對象改變時將自動通知其他對象,其他對象將作出反應。觀察者模式中,發生改變的對象稱為“觀察目標”,被通知的對象稱為“觀察者”。一個觀察目標可以有很多個觀察者。
觀察者模式定義如下:
觀察者模式:定義對象之間的一種一對多的依賴關系,使得每當一個對象狀態發生改變時,其相關依賴對象都得到通知并被自動更新。
觀察者模式又被稱為發布-訂閱模式(Publish-Subscribe)、模型-視圖模式(Model-View)、源-監聽器模式(Source-Listener)、從屬者模式(Dependents)。
2.觀察者模式結構
觀察者模式由觀察者和觀察目標組成,為便于擴展,兩個角色都設計了抽象層。觀察者模式的UML圖如下:
下述是觀察者模式的典型實現:
#define __DEMO_H__
#ifdef __DEMO_H__using namespace std;
#include <list>// 抽象觀察者
class Observer
{
public:virtual ~Observer() {}// 聲明響應更新方法virtual void update() = 0;
};// 具體觀察者
class ConcreteObserver : public Observer
{
public:// 實現響應更新方法void update(){// 具體操作}
};// 抽象目標
class Subject
{
public:virtual ~Subject() {}// 添加觀察者void attach(Observer *obs){obsList.push_back(obs);}// 移除觀察者void detach(Observer *obs){obsList.remove(obs);}// 聲明通知方法virtual void notify() = 0;protected:// 觀察者列表list<Observer *> obsList;
};// 具體目標
class ConcreteSubject : public Subject
{
public:// 實現通知方法void notify(){// 具體操作// 遍歷通知觀察者對象for (int i = 0; i < obsList.size(); i++){obsList[i]->update();}}
};// 客戶端代碼示例
int main()
{Subject *sub = new ConcreteSubject();Observer *obs = new ConcreteObserver();sub->attach(obs);sub->notify();delete sub;delete obs;return 0;
}
#endif
3.觀察者模式代碼實例
玩過和平精英這款游戲嗎?四人組隊絕地求生,當一個隊友發現物資時,可以發消息“我這里有物資”,其余三個隊友聽到后可以去取物資;當一個隊友遇到危險時,也可以發消息“救救我”,其余三個隊友得到消息后便立馬趕去營救。本例將用觀察者模式來模擬這個過程。
本例的UML圖如下:
本例中,抽象觀察者是Observer,聲明了發現物資或者需要求救時的呼叫的方法call(),具體觀察者是Player,即玩家,Player實現了呼叫call()方法,并且還定義了取物資come()和支援隊友help()的方法。本例定義了AllyCenter作為抽象目標,它維護了一個玩家列表playerList,并且定義了加入戰隊和剔除玩家的方法。 具體目標是聯盟中心控制器AllyCenterController,它實現了通知notify()方法,該方法將隊友call的消息傳達給玩家列表里的其余隊友,并作出相應的響應。
3.0.公共頭文件
通過一個枚舉類型來定義兩種消息類型,即發現物資和求助
#ifndef __COMMON_H__
#define __COMMON_H__enum INFO_TYPE{NONE,RESOURCE,HELP
};#endif //__COMMON_H__
3.1.觀察者
3.1.1.抽象觀察者Observer
class Observer {
public:virtual ~Observer() = default;virtual void call(INFO_TYPE, AllyCenter* ac) = 0;const std::string& getName() const { return name; }void setName(std::string n){ name = std::move(n); }protected:std::string name{"none"};
};
3.1.2.具體觀察者Player
// final: Player 不能再有子類
class Player final : public Observer {
public:Player() = default;explicit Player(std::string n) { setName(std::move(n)); }void call(INFO_TYPE, AllyCenter* ac) override; // 這里只聲明void help() const;void come() const;
};
3.2.目標類
3.2.1.抽象目標AllyCenter
// 前向聲明
// 不做前向聲明時,兩邊的頭文件往往互相 #include,形成“包含環”。 由于編譯是單向讀取的
class Observer;
// class Player;// 抽象目標:聯盟中心
class AllyCenter {
public:AllyCenter();virtual ~AllyCenter() {}// 聲明通知方法virtual void notify(INFO_TYPE infoType, const std::string& name) = 0;// 加入玩家void join(Observer *player);// 移除玩家void remove(Observer *player);protected:// 玩家列表std::vector<Observer*> playerList;
};
3.2.2.具體目標AllyCenterController
// 具體目標
class AllyCenterController : public AllyCenter {
public:AllyCenterController();// 實現通知方法void notify(INFO_TYPE infoType, const std::string& name) override;
};
循環包含
“循環包含”(include cycle)是指:頭文件彼此相互 #include(直接或間接 A→B→A)。
預處理展開時會卡在一個環里——雖然 include guard 會阻止“無限展開”,但副作用是某一邊在需要完整類型時只看到了前半張臉,于是報“未知類型 / 不完全類型(incomplete type)”之類的編譯錯誤。
錯誤示例
// A.h
#ifndef A_H
#define A_H
#include "B.h" // A 想用 B
struct A { B b; }; // ← 按值成員,必須要 B 的完整定義
#endif// B.h
#ifndef B_H
#define B_H
#include "A.h" // B 又包含 A
struct B { A a; }; // ← 也按值成員,需要 A 的完整定義
#endif
編譯時流程大致是:編譯器讀 A.h → 進 B.h → 試圖再進 A.h,但被 include guard 攔住,于是此時 B.h 里看不到 A 的完整定義,馬上報錯(反過來順序也一樣)。
“前向聲明什么時候不夠、必須 #include 對方頭?”
核心在于**“不完全類型 vs 完全類型”**。class X; 只是告訴編譯器“有個類型 X”,但看不到它的大小、布局和成員。凡是需要知道大小/布局/成員或能析構的場合,都必須拿到完整定義(所以要 #include “X.h”)。
3.3.客戶端代碼示例及效果
#include <cstdio>
#include <memory>
#include "Observer.h"
#include "AllyCenter.h"int main() {// 創建一個戰隊auto controller = std::make_unique<AllyCenterController>();// 創建4個玩家,并加入戰隊auto Jungle = std::make_unique<Player>("Jungle");auto Single = std::make_unique<Player>("Single");auto Jianmengtu = std::make_unique<Player>("Diego");auto SillyDog = std::make_unique<Player>("Richard");controller->join(Jungle.get());controller->join(Single.get());controller->join(Jianmengtu.get());controller->join(SillyDog.get());std::puts("");Jungle->call(RESOURCE, controller.get());std::puts("");SillyDog->call(HELP, controller.get());std::puts("");#ifdef _WIN32system("pause");
#endifreturn 0; // 智能指針自動釋放
}
上述代碼運行結果如下圖:
4.觀察者模式的應用
觀察者模式是一種使用頻率非常高的設計模式,幾乎無處不在。凡是涉及一對一、一對多的對象交互場景,都可以使用觀察者會模式。比如購物車,瀏覽商品時,往購物車里添加一件商品,會引起UI多方面的變化(購物車里商品數量、對應商鋪的顯示、價格的顯示等);各種編程語言的GUI事件處理的實現;所有的瀏覽器事件(mouseover,keypress等)都是使用觀察者模式的例子。
5.總結
之后我會持續更新,如果喜歡我的文章,請記得一鍵三連哦,點贊關注收藏,你的每一個贊每一份關注每一次收藏都將是我前進路上的無限動力 !!!↖(▔▽▔)↗感謝支持!