每當Jungle公衆號【Jungle筆記】更新發布了文章,作爲Jungle的關注者,你會第一時間接到消息,(如果)然後就可以去查看、點贊、評論和轉發,接下來的一天你都高高興興;
每當Jungle更新了CSDN博客,作爲Jungle的支持者,你也會在打開CSDN網站的時候看到消息,(如果)然後你就可以去查看、點贊、評論和轉發,接下來的一週你都高高興興。
也就是說,“Jungle更新發布文章”這個行爲可能會導致“關注者查看、評論、點贊”等一系列行爲。這表明“Jungle更新發布文章”並不是孤立的,而是與衆多對象產生了關聯。一個對象行爲的改變,其相關聯的對象都會得到通知,並自動產生對應的行爲。這在軟件設計模式中,即是觀察者模式。
1.觀察者模式簡介
軟件系統中的對象並不是孤立存在的,一個對象行爲的改變可能會引起其他所關聯的對象的狀態或行爲也發生改變,即“牽一髮而動全身”。觀察者模式建立了一種一對多的聯動,一個對象改變時將自動通知其他對象,其他對象將作出反應。觀察者模式中,發生改變的對象稱爲“觀察目標”,被通知的對象稱爲“觀察者”。一個觀察目標可以有很多個觀察者。
觀察者模式定義如下:
觀察者模式:
定義對象之間的一種一對多的依賴關係,使得每當一個對象狀態發生改變時,其相關依賴對象都得到通知並被自動更新。
觀察者模式又被稱爲發佈-訂閱模式(Publish-Subscribe)、模型-視圖模式(Model-View)、源-監聽器模式(Source-Listener)、從屬者模式(Dependents)。
2.觀察者模式結構
觀察者模式由觀察者和觀察目標組成,爲便於擴展,兩個角色都設計了抽象層。觀察者模式的UML圖如下:
- Subject(目標):是被觀察的對象,目標中定義了一個觀察者的集合,即一個目標可能會有多個觀察者,通過attach()和detach()方法來增刪觀察者對象。目標聲明瞭通知方法notify(),用於在自身狀態發生改變時通知觀察者。
- ConcreteSubject(具體目標):具體目標實現了通知方法notify(),同時具體目標有記錄自身狀態的屬性和成員方法;
- Observer(觀察者):觀察者將對接收到的目標發生改變的通知做出自身的反應,抽象層聲明瞭更新方法update();
- ConcreteObserver(具體觀察者): 實現了更新方法update(),具體觀察者中維護了一個具體目標對象的引用(指針),用於存儲目標的狀態。
下述是觀察者模式的典型實現:
#ifndef __DEMO_H__
#define __DEMO_H__
// 抽象觀察者
class Observer
{
public:
// 聲明響應更新方法
virtual void update() = 0;
};
// 具體觀察者
class ConcreteObserver:public Observer
{
public:
// 實現響應更新方法
void update(){
// 具體操作
}
};
// 抽象目標
class Subject
{
public:
// 添加觀察者
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();
return 0;
}
#endif //__DEMO_H__
3.觀察者模式代碼實例
玩過和平精英這款遊戲嗎?四人組隊絕地求生,當一個隊友發現物資時,可以發消息“我這裏有物資”,其餘三個隊友聽到後可以去取物資;當一個隊友遇到危險時,也可以發消息“救救我”,其餘三個隊友得到消息後便立馬趕去營救。本例Jungle將用觀察者模式來模擬這個過程。
本例的UML圖如下:
本例中,抽象觀察者是Observer,聲明瞭發現物資或者需要求救時的呼叫的方法call(),具體觀察者是Player,即玩家,Player實現了呼叫call()方法,並且還定義了取物資come()和支援隊友help()的方法。本例定義了AllyCenter作爲抽象目標,它維護了一個玩家列表playerList,並且定義了加入戰隊和剔除玩家的方法。具體目標是聯盟中心控制器AllyCenterController,它實現了通知notify()方法,該方法將隊友call的消息傳達給玩家列表裏的其餘隊友,並作出相應的響應。源代碼見https://github.com/FengJungle/DesignPattern。
3.0.公共頭文件
通過一個枚舉類型來定義兩種消息類型,即發現物資和求助
#ifndef __COMMON_H__
#define __COMMON_H__
enum INFO_TYPE{
NONE,
RESOURCE,
HELP
};
#endif //__COMMON_H__
3.1.觀察者
3.1.1.抽象觀察者Observer
// 抽象觀察者 Observer
class Observer
{
public:
Observer(){}
// 聲明抽象方法
virtual void call(INFO_TYPE infoType, AllyCenter* ac) = 0;
string getName(){
return name;
}
void setName(string iName){
this->name = iName;
}
private:
string name;
};
3.1.2.具體觀察者Player
// 具體觀察者
class Player :public Observer
{
public:
Player(){
setName("none");
}
Player(string iName){
setName(iName);
}
// 實現
void call(INFO_TYPE infoType, AllyCenter* ac){
switch (infoType){
case RESOURCE:
printf("%s :我這裏有物資\n", getName().c_str());
break;
case HELP:
printf("%s :救救我\n", getName().c_str());
break;
default:
printf("Nothing\n");
}
ac->notify(infoType, getName());
}
// 實現具體方法
void help(){
printf("%s:堅持住,我來救你!\n", getName().c_str());
}
void come(){
printf("%s:好的,我來取物資\n", getName().c_str());
}
};
3.2.目標類
3.2.1.抽象目標AllyCenter
聲明
// 抽象目標:聯盟中心
class AllyCenter
{
public:
AllyCenter();
// 聲明通知方法
virtual void notify(INFO_TYPE infoType, std::string name) = 0;
// 加入玩家
void join(Observer* player);
// 移除玩家
void remove(Observer* player);
protected:
// 玩家列表
std::vector<Observer*>playerList;
};
實現
#include "AllyCenter.h"
#include "Observer.h"
AllyCenter::AllyCenter(){
printf("大吉大利,今晚喫雞!\n");
}
// 加入玩家
void AllyCenter::join(Observer* player){
if (playerList.size() == 4){
printf("玩家已滿\n");
return;
}
printf("玩家 %s 加入\n", player->getName().c_str());
playerList.push_back(player);
if (playerList.size() == 4){
printf("組隊成功,不要慫,一起上!\n");
}
}
// 移除玩家
void AllyCenter::remove(Observer* player){
printf("玩家 %s 退出\n", player->getName().c_str());
//playerList.remove(player);
}
3.2.2.具體目標AllyCenterController
聲明:
// 具體目標
class AllyCenterController :public AllyCenter
{
public:
AllyCenterController();
// 實現通知方法
void notify(INFO_TYPE infoType, std::string name);
};
實現:
AllyCenterController::AllyCenterController(){}
// 實現通知方法
void AllyCenterController::notify(INFO_TYPE infoType, std::string name){
switch (infoType){
case RESOURCE:
for each (Observer* obs in playerList){
if (obs->getName() != name){
((Player*)obs)->come();
}
}
break;
case HELP:
for each (Observer* obs in playerList){
if (obs->getName() != name){
((Player*)obs)->help();
}
}
break;
default:
printf("Nothing\n");
}
}
3.3.客戶端代碼示例及效果
#include "Observer.h"
#include "AllyCenter.h"
int main()
{
// 創建一個戰隊
AllyCenterController* controller = new AllyCenterController();
// 創建4個玩家,並加入戰隊
Player* Jungle = new Player("Jungle");
Player* Single = new Player("Single");
Player* Jianmengtu = new Player("賤萌兔");
Player* SillyDog = new Player("傻子狗");
controller->join(Jungle);
controller->join(Single);
controller->join(Jianmengtu);
controller->join(SillyDog);
printf("\n\n");
// Jungle發現物資,呼叫隊友
Jungle->call(RESOURCE, controller);
printf("\n\n");
// 傻子狗遇到危險,求救隊友
SillyDog->call(HELP, controller);
printf("\n\n");
system("pause");
return 0;
}
上述代碼運行結果如下圖:
4.觀察者模式的應用
觀察者模式是一種使用頻率非常高的設計模式,幾乎無處不在。凡是涉及一對一、一對多的對象交互場景,都可以使用觀察者會模式。比如購物車,瀏覽商品時,往購物車裏添加一件商品,會引起UI多方面的變化(購物車裏商品數量、對應商鋪的顯示、價格的顯示等);各種編程語言的GUI事件處理的實現;所有的瀏覽器事件(mouseover,keypress等)都是使用觀察者模式的例子。
5.總結
優點:
- 觀察者模式實現了穩定的消息更新和傳遞的機制,通過引入抽象層可以擴展不同的具體觀察者角色;
- 支持廣播通信,所有已註冊的觀察者(添加到目標列表中的對象)都會得到消息更新的通知,簡化了一對多設計的難度;
- 符合開閉原則,增加新的觀察者無需修改已有代碼,在具體觀察者與觀察目標之間不存在關聯關係的情況下增加新的觀察目標也很方便。
缺點:
- 代碼中觀察者和觀察目標相互引用,存在循環依賴,觀察目標會觸發二者循環調用,有引起系統崩潰的風險;
- 如果一個觀察目標對象有很多直接和簡介觀察者,將所有的觀察者都通知到會耗費大量時間。
適用環境:
- 一個對象的改變會引起其他對象的聯動改變,但並不知道是哪些對象會產生改變以及產生什麼樣的改變;
- 如果需要設計一個鏈式觸發的系統,可是使用觀察者模式;
- 廣播通信、消息更新通知等場景。
歡迎關注知乎專欄:Jungle是一個用Qt的工業Robot
歡迎關注Jungle的微信公衆號:Jungle筆記