1. 程式人生 > >第21章 行為型模式—觀察者模式

第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)觀察者與中介者模式

  如把一個介面所有的事件用一箇中介者物件封裝處理,當一個元件觸發事件以後,只需要通知中介者,由於中介者封裝了需要操作其他元件的動作。這樣就可以實現目標物件與觀察者之間的聯動。