java設計模式 - 觀察者模式

一、 概述

觀察者模式是對象的行為模式,又叫發布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。
觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。

將一個系統分割成一個一些類相互協作的類有一個不好的副作用,那就是需要維護相關對象間的一致性。我們不希望為了維持一致性而使各類緊密耦合,這樣會給維護、擴展和重用都帶來不便。觀察者就是解決這類的耦合關系的。

二、 觀察者模式結構

觀察者模式類圖

涉及到的角色分別為

  • 抽象主題(Subject)角色:抽象主題角色把所有對觀察者對象的引用保存在一個聚集(比如ArrayList對象)里,每個主題都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和刪除觀察者對象,抽象主題角色又叫做抽象被觀察者(Observable)角色。
  • 具體主題(ConcreteSubject)角色:將有關狀態存入具體觀察者對象;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。
  • 抽象觀察者(Observer)角色:為所有的具體觀察者定義一個接口,在得到主題的通知時更新自己,這個接口叫做更新接口。
  • 具體觀察者(ConcreteObserver)角色:存儲與主題的狀態自恰的狀態。具體觀察者角色實現抽象觀察者角色所要求的更新接口,以便使本身的狀態與主題的狀態 像協調。如果需要,具體觀察者角色可以保持一個指向具體主題對象的引用。

三、 代碼示例

抽象觀察者

提供觀察者狀態變更方法

public interface Observer {
    /**
     * 更新觀察者狀態方法
     * @param state 新的狀態
     */
    public void update(String state);
}

抽象主題者

抽象主題定義觀察者對象列表,提供新增和刪除觀察者方法,并通知觀察者狀態變化

public abstract class Subject {
    /**
     * 觀察者對象列表
     */
    private List<Observer> list = new ArrayList<Observer>();

    /**
     * 新增觀察者,將觀察者對象放入觀察者對象列表中
     * @param observer 觀察者對象
     */
    public void attach (Observer observer) {
        list.add(observer);
        System.out.println("Attached new observer");
    }

    /**
     * 刪除觀察者,從觀察者對象列表中刪除觀察者
     * @param observer 觀察者對象
     */
    public void detach (Observer observer) {
        list.remove(observer);
        System.out.println("Detach a observer");
    }

    /**
     * 通知觀察者狀態變更,逐個遍歷觀察者對象列表,調用觀察者狀態變更方法
     * @param newState 新的狀態
     */
    public void nodifyObservers(String newState) {
        for (Observer observer : list) {
            observer.update(newState);
        }
    }
}

具體主題對象

提供狀態定義,狀態變更方法,調用觀察者通知方法

public class ConcreateSubject extends Subject {
    /**
     * 對象狀態
     */
    private String state;

    /**
     * 獲取狀態
     * @return 返回狀態值
     */
    public String getState() {
        return state;
    }

    /**
     * 狀態變更方法,修改狀態,并將狀態通知觀察者對象
     * @param newState 新的狀態
     */
    public void change(String newState) {
        this.state = newState;
        System.out.println("State change to " + this.state);
        this.nodifyObservers(this.state);
    }
}

具體觀察者對象

定義觀察者狀態,實現具體處理業務

public class ConcreateObserver implements Observer {
    //觀察者狀態
    private String observerState;

    /**
     * 更新觀察者狀態方法
     * @param state 新的狀態
     */
    public void update(String state) {
        this.observerState = state;
        System.out.println("Observer state is " + this.observerState);
    }
}

在創建了一個客戶端調用代碼后:

public class Client {
    public static void main(String[] args) {
        //創建具體主題角色類的實例
        ConcreateSubject subject = new ConcreateSubject();

        //創建觀察者角色實例
        Observer observer = new ConcreateObserver();

        //將觀察者對象登記到主題角色對象上
        subject.attach(observer);

        //修改主題角色狀態
        subject.change("State1");
    }
}

執行結果為:

Attached new observer
State change to State1
Observer state is State1

首先需要創建具體主題對象,創建觀察者對象,并將觀察者對象注冊到主題對象中。
然后在調用主題對象的修改狀態方法時,先會修改主題對象中狀態值,然后調用父類中觀察者通知方法nodifyObservers(),逐個循環通知所有觀察者列表中的觀察者狀態變更。

推模型和拉模型

在觀察者模式中,又分為推模型和拉模型兩種方式。

  • 推模型
    主題對象向觀察者推送主題的詳細信息,不管觀察者是否需要,推送的信息通常是主題對象的全部或部分數據。
  • 拉模型
    主題對象在通知觀察者的時候,只傳遞少量信息。如果觀察者需要更具體的信息,由觀察者主動到主題對象中獲取,相當于是觀察者從主題對象中拉數據。一般這種模型的實現中,會把主題對象自身通過update()方法傳遞給觀察者,這樣在觀察者需要獲取數據的時候,就可以通過這個引用來獲取了。

根據上面的描述,發現前面的例子就是典型的推模型,下面給出一個拉模型的實例。

拉模型的抽象觀察者類

拉模型通常都是把主題對象當做參數傳遞。

public interface ObserverGet {
    /**
     * 直接傳入主題對象,觀察者使用主題對象獲取主題對象中需要的信息
     * @param subject 主題對象
     */
    public void update(Subject subject);
}

拉模型的具體觀察者對象

public class ConcreateObserverGet implements ObserverGet {
    private String observerState;
    public void update(Subject subject) {
        //保證對象同目標對象類型一致
        observerState = ((ConcreateSubject)subject).getState();

        System.out.println("ObserverGet state is " + this.observerState);
    }
}

拉模型的抽象主題對象

由于拉模型獲取的內容為具體的主題對象,因此需要在推送主題對象狀態時,給出具體主題對象,由觀察者對象根據具體主題對象進行業務處理。

public abstract class Subject {
    /**
     * 觀察者對象列表
     */
    private List<ObserverGet> list = new ArrayList<ObserverGet>();

    /**
     * 新增觀察者,將觀察者對象放入觀察者對象列表中
     * @param observerGet 觀察者對象
     */
    public void attach (ObserverGet observerGet) {
        list.add(observerGet);
        System.out.println("Attached new observer");
    }

    /**
     * 刪除觀察者,從觀察者對象列表中刪除觀察者
     * @param observerGet 觀察者對象
     */
    public void detach (ObserverGet observerGet) {
        list.remove(observerGet);
        System.out.println("Detach a observer");
    }

    /**
     * 通知觀察者狀態變更,逐個遍歷觀察者對象列表,調用觀察者狀態變更方法
     */
    public void nodifyObservers() {
        for (ObserverGet observer : list) {
            observer.update(this);
        }
    }
}

執行結果為:

Attached new observer
State change to State1
ObserverGet state is State1

同推模型相比,拉模型在進行觀察者狀態通知時,就不需要傳遞參數,直接傳遞對象本身即可。

兩種模式的比較

  • 推模型是假定主題對象知道觀察者需要的數據;而拉模型是主題對象不知道觀察者具體需要什么數據,沒有辦法的情況下,干脆把自身傳遞給觀察者,讓觀察者自己去按需要取值。
  • 推模型可能會使得觀察者對象難以復用,因為觀察者的update()方法是按需要定義的參數,可能無法兼顧沒有考慮到的使用情況。這就意味著出現新情況的時候,就可能提供新的update()方法,或者是干脆重新實現觀察者;而拉模型就不會造成這樣的情況,因為拉模型下,update()方法的參數是主題對象本身,這基本上是主題對象能傳遞的最大數據集合了,基本上可以適應各種情況的需要。

JAVA提供的對觀察者模式的支持

在JAVA語言的java.util庫里面,提供了一個Observable類以及一個Observer接口,構成JAVA語言對觀察者模式的支持。

Observer接口

這個接口只定義了一個方法,即update()方法,當被觀察者對象的狀態發生變化時,被觀察者對象的notifyObservers()方法就會調用這一方法。

public interface Observer {
    void update(Observable o, Object arg);
}

Observable類

被觀察者類都是java.util.Observable類的子類。java.util.Observable提供公開的方法支持觀察者對象,這些方法中有兩個對Observable的子類非常重要:一個是setChanged(),另一個是notifyObservers()。
第一方法setChanged()被調用之后會設置一個內部標記變量,代表被觀察者對象的狀態發生了變化。第二個是notifyObservers(),這個方法被調用時,會調用所有登記過的觀察者對象的update()方法,使這些觀察者對象可以更新自己。

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;

    public Observable() {
        obs = new Vector<>();
    }

    /**
     * 新增一個觀察者對象到觀察者對象列表中,如果觀察者對象列表為空,則提示空指針異常
     */
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    /**
     * 從觀察者對象列表中刪除一個觀察者對象
     */
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }

    public void notifyObservers() {
        notifyObservers(null);
    }

    /**
     * 如果調用方法`hasChanged()`表明對象已改變,則通知所有注冊觀察者對象
     * 通過調用觀察者對象的`update()`方法。
     * 然后調用方法`clearChanged()`復位對象修改狀態,重新開始判斷
     */
    public void notifyObservers(Object arg) {
        /*
         * 一個數組,保存當前正在進行監控的觀察者對象
         */
        Object[] arrLocal;

        synchronized (this) {
            /* 我們不希望觀察者對象在保持監控的時候回調任何代碼
             * 這個方法將會提取每個觀察者對象的狀態,因此需要使用同步事務鎖
             * 但是通知觀察者對象方法不需要
             * 最糟糕的情況是下面兩種
             * 1) 新增的觀察者對象將會錯過正在進行中的通知
             * 2) 一個最近未注冊的觀察者將在其不需要的時候收到通知
             */
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    /**
     * 清空觀察者對象列表
     */
    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }

    /**
     * 當被觀察的對象,也就是主題對象發生變更時,調用此方法
     * 然后調用`hasChanged()`方法就會返回true
     */
    protected synchronized void setChanged() {
        changed = true;
    }

    /**
     * 重置變化狀態為未變化
     */
    protected synchronized void clearChanged() {
        changed = false;
    }

    /**
     * 判斷主題對象是否發生改變
     */
    public synchronized boolean hasChanged() {
        return changed;
    }

    /**
     * 得到觀察者對象列表中觀察者對象數量
     */
    public synchronized int countObservers() {
        return obs.size();
    }
}

這個類代表一個被觀察者對象,有時稱之為主題對象。一個被觀察者對象可以有數個觀察者對象,每個觀察者對象都是實現Observer接口的對象。在被觀察者發生變化時,會調用Observable的notifyObservers()方法,此方法調用所有的具體觀察者的update()方法,從而使所有的觀察者都被通知更新自己。

怎樣使用JAVA對觀察者模式的支持

這里給出一個非常簡單的例子,說明怎樣使用JAVA所提供的對觀察者模式的支持。
在這個例子中,被觀察對象叫做Watched;而觀察者對象叫做Watcher。Watched對象繼承自java.util.Observable類;而Watcher對象實現了java.util.Observer接口。另外有一個Test類扮演客戶端角色。

被觀察者對象類,繼承Observable

public class Watched extends Observable {
    private String data;
    public String getData() {
        return data;
    }
    public void setData(String data) {
        if (this.data != data) {
            this.data = data;
            //對象值發生變更,設置對象已發生改變
            setChanged();
            System.out.println("Data changed to : " + data);
        }
        //通知觀察者對象們
        notifyObservers();
    }
}

創建具體觀察者對象類,實現Observer接口

public class Watcher implements java.util.Observer {
    public Watcher(Observable observable) {
        observable.addObserver(this);
    }
    public void update(Observable o, Object arg) {
        System.out.println("Data hasChanged to : " + ((Watched)o).getData());
    }
}

測試代碼

public class TestObserver {
    public static void main(String[] args) {
        //創建被觀察對象,也就是具體主題對象
        Watched watched = new Watched();
        //創建具體觀察者對象,并且將觀察者對象添加到被觀察者對象的觀察者兌現列表中
        java.util.Observer watcher = new Watcher(watched);

        //修改被觀察者對象內容
        watched.setData("START");
        watched.setData("END");
        watched.setData("STOP");
    }
}

執行結果為:

Data changed to : START
Data hasChanged to : START
Data changed to : END
Data hasChanged to : END
Data changed to : STOP
Data hasChanged to : STOP

首先創建被觀察對象Watched,然后創建觀察者對象watcher,并且調用構造方法,添加到被觀察者對象的觀察者對象列表中。
在被觀察對象修改內容時,在內容變化的情況下,修改被觀察對象的變更狀態為已改變。然后調用通知觀察者對象方法notifyObservers,傳遞自身對象。
觀察者對象接受到update方法的調用后,處理被觀察者對象,并獲取其中內容。

參考:
《JAVA與模式》之觀察者模式

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容