第21章 行為型模式—觀察者模式
1. 觀察者模式(Observer Pattern)的定義
(1)定義:定義物件間的一種一對多的依賴關係。當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。
(2)觀察者模式的結構和說明(拉模型)
①Subject:目標物件,通常具如的功能:一個目標可以被多個觀察者觀察;目標提供對觀察者的註冊和退訂的維護;當目標的狀態發生變化時,目標負責通知所有註冊的、有效的觀察者。
②Observer:定義觀察者的介面,提供目標通知時對應的更新方法,這個更新方法進行相應的業務處理,可以在這個方法裡面回撥目標物件,以獲取目標物件的資料。
③ConcreteSubject:具體的目標實現物件,用來維護目標狀態,當目標物件的狀態發生改變時,通知所有註冊的、有效的觀察者,讓觀察者執行相應的處理。
④ConcreteObserver:觀察者的具體實現物件,用來接收目標的通知,並進行相應的後續處理,比如更新自身狀態以保持和目標的相應狀態一致。
【程式設計實驗】圖書促銷活動
//行為型模式——觀察者模式
//場景:圖書促銷活動(拉模型)
/*
觀察者模式在現實的應用系統中也有好多應用,比如像噹噹網、京東商城一類的電子商務網站,
如果你對某件商品比較關注,可以放到收藏架,那麼當該商品降價時,系統給您傳送手機簡訊或郵件。
這就是觀察者模式的一個典型應用,商品是被觀察者,有的叫主體;關注該商品的客戶就是觀察者
*/
#include <iostream>
#include <string>
#include <list>
using namespace std;
class Subject; //前向宣告(被觀察者)
//******************************觀察者介面********************************
class Observer
{
public:
virtual void update(Subject* subject) = 0;
};
//******************************目標物件(Subject)***************************
//抽象目標物件(被觀察者)
class Subject //Java中命名為Observable
{
private:
bool changed;
list<Observer*> obs;
public:
void setChanged()
{
changed = true;
}
void clearChanged()
{
changed = false;
}
bool hasChanged()
{
return changed;
}
void addObserver(Observer* o)
{
obs.push_back(o);
}
void delObserver(Observer* o)
{
obs.remove(o);
}
void notifyObservers()
{
list<Observer*>::iterator iter = obs.begin();
while(iter != obs.end())
{
(*iter)->update(this);
++iter;
}
}
};
//書——被觀察者
class Book : public Subject
{
private:
string name;
double price;
public:
string& getName()
{
return name;
}
void setName(string value)
{
name = value;
}
double getPrice()
{
return price;
}
void setPrice(double value)
{
price = value;
}
//當書的價格修改時呼叫該方法
void modifyPrice()
{
//呼叫父類方法,改變被觀察者的狀態
setChanged();
//通知客戶該書己降價
notifyObservers();
}
};
//*****************************具體的觀察者*****************************
//具體觀察者(一般顧客,假設只留手機號碼)
class Buyer : public Observer
{
private:
string buyerId;
string mobileNo;
public:
string& getBuyerId(){return buyerId;}
void setBuyerId(string value) {buyerId = value;}
string& getMobileNo(){return mobileNo;}
void setMobileNo(string value){mobileNo = value;}
//收到通知時要進行的操作
void update(Subject* s)
{
Book* b = (Book*)s;
cout <<"給一般顧客(" <<buyerId <<")發的手機簡訊:"<< b->getName()
<<"降價了,目前的價格為" << b->getPrice()<<"元" << endl;
}
};
//具體觀察者(新華書店,假設只留email)
class BookStore : public Observer
{
private:
string buyerId;
string email;
public:
string& getBuyerId(){return buyerId;}
void setBuyerId(string value) {buyerId = value;}
string& getEmail(){return email;}
void setEmail(string value){email = value;}
//收到通知時要進行的操作
void update(Subject* s)
{
Book* b = (Book*)s;
cout <<"給新華書店(" <<buyerId <<")發的電子郵件:"<< b->getName()
<<"降價了,目前的價格為" << b->getPrice()<<"元" << endl;
}
};
int main()
{
//書促銷活動
Book bk;
bk.setName("《設計模式:可複用面向物件軟體的基礎》");
bk.setPrice(45.00); //假設原價是60,現在是降價促銷
//一般客戶
Buyer by;
by.setBuyerId("001");
by.setMobileNo("1359912XXXX");
//新華書店
BookStore bs;
bs.setBuyerId("002");
bs.setEmail(" [email protected]");
//增加觀察者,在實際應用中就是哪些人對該書做了關注
bk.addObserver(&by); //一般客戶對該對做了關注
bk.addObserver(&bs); //新華書店對該書做了關注
//傳送降價通知
bk.modifyPrice();
return 0;
}
/*
給一般顧客(001)發的手機簡訊:《設計模式:可複用面向物件軟體的基礎》降價了,目前的價格為45元
給新華書店(002)發的電子郵件:《設計模式:可複用面向物件軟體的基礎》降價了,目前的價格為45元
*/
2. 思考觀察者模式
(1)觀察者模式的本質
(2)目標物件和觀察者之間的關係
①一個目標只有一個觀察者,也可以被多個觀察者觀察。
②一個觀察者可以觀察多個目標物件,但這裡一般觀察者內要提供不同的update方法,以便讓不同的目標物件來回調。
③在觀察者模式中,觀察者和目標是單向依賴的,只有觀察者依賴於目標,而目標是不依賴於具體的觀察者(只依賴於介面)。
④它們之間的聯絡主動權掌握在目標物件手中,只有目標物件知道什麼時候需要通知觀察者。在整個過程中,觀察者始終是被動地等待目標物件的通知。
⑤對於目標物件而言,所有的觀察者都是一樣的,會一視同仁。但也可以在目標物件裡面進行控制,實現有區別的對待觀察者。
(3)基本的實現說明
①具體的目標實現物件要能維護觀察者的註冊資訊,最簡單的實現方案就是採用連結串列來儲存觀察者的註冊資訊。
②具體的目標實現物件需要維護引起通知的狀態,一般情況下是目標自身的狀態。變形使用的情況下,也可以是別的物件的狀態。
③具體的觀察者實現物件需要能接收目標的通知,能夠接收目標傳遞的資料或主動去獲取目標的資料,並進行後續處理。
④如果是一個觀察者觀察多個目標,那在觀察者的更新方法裡面,需要去判斷是來自哪一個目標的通知。一種簡音的解決方案是擴充套件update方法,比哪裡在方法裡多傳遞一個引數進行區分。還有一種更簡單的方法,就是定義不同的回撥方法。
(4)觸發通知的時機
①一般是在完成狀態維護後觸發,因為通知會傳遞資料,不能夠先通知後改資料,這很容易導致觀察者和目標物件的狀態不一致。
②可能出錯的示例程式碼片段
(5)相互觀察
在某些應用中,可能會出現目標和觀察者相互觀察的情況。這種情況要防止可能出現的死迴圈現象。
3. 推模型和拉模型
(1)推模型:目標物件主動向觀察者推送目標的詳細資訊,不管觀察者是否需要,推送的訊息通常是目標物件的全部或部分資料。相當於是在廣播通知。
(2)拉模型:目標物件在通知觀察者的時候,只傳遞少量資訊。如果觀察者需要更具體的資訊,由觀察者主動到目標物件中獲取,相當於是觀察者從目標物件中拉資料。一般這種模型的實現中,會把目標物件自己通過update方法傳遞給觀察者,這樣在觀察者需要獲取資料的時候,就可以通過這個引用來獲取。
(3)關於兩種模型的比較
①推模型是假定目標物件知道觀察者需要的資料;而拉模型是目標物件不知道觀察者具體需要什麼資料,在沒有辦法的情況下,乾脆把自身傳遞給觀察者,讓觀察者自己去按需取值。
②推模式可能會使觀察者物件難以複用,因為觀察者定義的update方法是按需定義的,可能無法兼顧沒有考慮到的情況。這意味者出現新的情槳葉時,就可以需要提供新的update方法。或乾脆重新實現觀察者。而拉模式不會造成這種情況,因為update方法的引數是目標物件本身。這基本上是目標物件能傳遞的最大資料集合,基本可以適應各種情況的需要。
【程式設計實驗】模擬事件監聽系統(推模型)
//行為型模式——觀察者模式
//場景——模擬事件監聽系統(推模型)
//Switch:事件源(開關),相當於具體的Subject角色
//EventListener:事件監聽介面,相當於Observer角色
//Light:監聽者,相當於具體的Observer角色
//SwitchEvent:事件物件,用於當事件發生時向監聽者傳送的資料型別(含事件源物件,源的狀態等)。
#include <iostream>
#include <string>
#include <list>
using namespace std;
class Switch; //前向宣告
//******************************輔助類(用於事件源向監聽者傳遞的資料型別)********************
typedef void Object;
//事件類(主要用來記錄觸發事件的源物件)
class EventObject
{
Object* source; //記錄事件源物件
public:
EventObject(Object* source)
{
this->source = source;
}
Object* EventSource()
{
return source;
}
};
//開關事件,用於向監聽者發生的資料
class SwitchEvent: public EventObject
{
string switchState; //表示開關的狀態
public:
SwitchEvent(Switch* source, string switchState):EventObject(source)
{
this->switchState = switchState;
}
void setSwitchState(string switchState)
{
this->switchState = switchState;
}
string& getSwitchState()
{
return switchState;
}
};
//***************************************Observer(觀察者)***************************
class EventListener
{
public:
virtual void handleEvent(SwitchEvent* switchEvent) = 0;
};
//具體的監聽者(相當於具體觀察者)
class Light : public EventListener
{
public:
void handleEvent(SwitchEvent* switchEvent)
{
cout <<"the light receive a switch(" << switchEvent->EventSource()
<<") emit \""<<switchEvent->getSwitchState() <<"\" signal"<< endl;
}
};
//****************************************Subject(被觀察者)***********************
//抽象目標物件類
class Subject
{
list<EventListener*> switchListeners;
public:
void addListener(EventListener* listener)
{
switchListeners.push_back(listener);
}
void removeListener(EventListener* listener)
{
switchListeners.remove(listener);
}
void notifyListeners(SwitchEvent* switchEvent)
{
list<EventListener*>::iterator iter = switchListeners.begin();
while(iter != switchListeners.end())
{
(*iter)->handleEvent(switchEvent);
++iter;
}
}
};
//具體的目標物件,電源開關(事件源物件)
class Switch : public Subject
{
SwitchEvent* switchEvent;
public:
Switch()
{
switchEvent = new SwitchEvent(this, "close");
}
void open()
{
switchEvent->setSwitchState("open");
notifyListeners(switchEvent);
}
void close()
{
switchEvent->setSwitchState("close");
notifyListeners(switchEvent);
}
~Switch()
{
delete switchEvent;
}
};
int main()
{
Switch sw; //開關,被監聽物件
Light lg; //燈,監聽器
sw.addListener(&lg); //加入開關的監聽佇列
//開啟Switch
sw.open();
//關閉Switch
sw.close();
return 0;
}
/*
the light receive a switch(0x23fea4) emit "open" signal
the light receive a switch(0x23fea4) emit "close" signal
*/
4. 觀察者模式的優缺點
(1)優點
①觀察者模式實現了觀察者和目標之間的抽象耦合
②實現了動態聯動。由於觀察者模式對觀察者的註冊實行管理,那就可以在執行期間,通過動態地控制註冊的觀察者,來控制某個動作的聯動範圍,從而實現動態聯動。
③支援廣播通訊
(2)缺點
可能會引起無謂的操作。由於觀察者模式每次都是廣播通訊,不管觀察者需不需要,每個觀察者都會被呼叫update方法。如果觀察者不需要執行相應處理,那這次操作就浪費了,甚至可能會誤操作。如本應在執行這次狀態更新前把某個觀察者刪除掉,但現在這個觀察者都還沒刪除,訊息就又到達了,那麼就會引起誤操作。
5. 觀察者模式的應用場景
(1)聊天室程式,伺服器轉發給所有客戶端,群發訊息等
(2)網路遊戲(多人聯機對戰)場景中,伺服器將客戶端的狀態進行分發。
(3)事件處理模型,基於觀察者模式的委派事件模型(事件源:目標物件;事件監聽器:觀察者)
【程式設計實驗】區別對待觀察者(變式觀察者模式)
//行為型模式——觀察者模式
//場景:水質監測系統(拉模型)
/*、
說明:
1、水質正常時:只通知監測人員做記錄
2、輕度汙染時:除了通知監測人員做記錄外,還要通知預警人員,判斷是否需要預警
3、中度或重度汙染時:除了通知以上兩人種外,還要通知部門領導做相應的處理
解決方式:
1、每次汙染時,目標可以通知所有觀察者,由觀察者決定是否屬自己處理的情況
2、每次汙染時,在目標裡進行判斷,然後只通知相應的觀察者(本例採用這種方式)
*/
#include <iostream>
#include <string>
#include <list>
using namespace std;
class WatcherObserver; //前向宣告
//定義水質監測的目標物件
class WaterQualitySubject
{
protected:
list<WatcherObserver*> obs;
public:
void attach(WatcherObserver* observer)
{
obs.push_back(observer);
}
void detach(WatcherObserver* observer)
{
obs.remove(observer);
}
//通知相應的觀察者物件(這裡為抽象方法,由子類去實現區別物件觀察者)
virtual void notifyWatchers() = 0;
//獲取水質汙染的級別
virtual int getPolluteLevel() = 0;
};
//水質觀察者介面定義
class WatcherObserver
{
public:
//被通知時的處理方法,引數為被觀察的目標物件
virtual void update(WaterQualitySubject* subject) = 0;
//設定和獲取觀察人員的職務
virtual void setJob(string job) = 0;
virtual string& getJob() = 0;
};
//具體的觀察者
class Watcher : public WatcherObserver
{
private:
string job;
public:
void setJob(string job){this->job = job;}
string& getJob(){return job;}
//收到通知時的處理過程
void update(WaterQualitySubject* subject) //拉模型
{
cout <<job << "獲取到通知,當前汙染級別為:"
<< subject->getPolluteLevel() << endl;
}
};
//具體的水質監測物件
class WaterQuality : public WaterQualitySubject
{
private:
int polluteLevel; //0正常,1輕度汙染,2中度汙染,3重度汙染
public:
WatcherQuality(){polluteLevel = 0;}
int getPolluteLevel()
{
return polluteLevel;
}
void setPolluteLevel(int value)
{
polluteLevel = value;
notifyWatchers(); //通知相應的觀察者
}
//通知相應的觀察者物件
void notifyWatchers()
{
list<WatcherObserver*>::iterator iter = obs.begin();
while (iter != obs.end())
{
//根據汙染級別判斷是否需要通知
//通知監測員記錄
if(polluteLevel >=0)
{
if((*iter)->getJob()=="監測人員")
{
(*iter)->update(this);
}
}
//通知預警人員
if(polluteLevel >=1)
{
if((*iter)->getJob()=="預警人員")
{
(*iter)->update(this);
}
}
//通知監測部門領導
if(polluteLevel >=2)
{
if((*iter)->getJob()=="監測部門領導")
{
(*iter)->update(this);
}
}
++iter;
}
}
};
int main()
{
//建立水質主題物件
WaterQuality subject;
//建立幾個觀察者
WatcherObserver* watcher1 = new Watcher();
watcher1->setJob("監測人員");
WatcherObserver* watcher2 = new Watcher();
watcher2->setJob("預警人員");
WatcherObserver* watcher3 = new Watcher();
watcher3->setJob("監測部門領導");
//註冊觀察者
subject.attach(watcher1);
subject.attach(watcher2);
subject.attach(watcher3);
//填寫水質報告
cout << "當水質正常的時候---------------------------" << endl;
subject.setPolluteLevel(0);
cout << endl;
cout << "當水質輕度汙染的時候-----------------------" << endl;
subject.setPolluteLevel(1);
cout << endl;
cout << "當水質中度汙染的時候-----------------------" << endl;
subject.setPolluteLevel(2);
delete watcher1;
delete watcher2;
delete watcher3;
return 0;
}
/*輸出結果:
當水質正常的時候---------------------------
監測人員獲取到通知,當前汙染級別為:0
當水質輕度汙染的時候-----------------------
監測人員獲取到通知,當前汙染級別為:1
預警人員獲取到通知,當前汙染級別為:1
當水質中度汙染的時候-----------------------
監測人員獲取到通知,當前汙染級別為:2
預警人員獲取到通知,當前汙染級別為:2
監測部門領導獲取到通知,當前汙染級別為:2
*/
6. 相關模式
(1)觀察者與狀態模式
①這兩者有相似之處。觀察者模式是當目標狀態發生改變時,觸發並通知觀察者,讓觀察者去執行相應的操作。而狀態模式是根據不同的狀態,選擇不同的實現,這個實現類的主要功能是針對狀態進行相應的操作,它不像觀察者,觀察者本身還有很多其他的功能,接收通知並執行相應處理只是觀察者的部分功能。
②這兩者可以結合使用。觀察者模式的重心在觸發聯動,但到底決定哪些觀察者會被聯動,這裡可以採用狀態模式來實現,也可以使用策略模式來選擇需要聯動的觀察者。
(2)觀察者與中介者模式
如把一個介面所有的事件用一箇中介者物件封裝處理,當一個元件觸發事件以後,只需要通知中介者,由於中介者封裝了需要操作其他元件的動作。這樣就可以實現目標物件與觀察者之間的聯動。