設(shè)計(jì)模式之【觀察者模式】

用最簡單的一句話來理解觀察者模式就是:當(dāng)一個(gè)對象發(fā)生改變時(shí),其相關(guān)依賴對象皆得到通知并被自動更新。
類圖
關(guān)于這個(gè)圖的四個(gè)對象有如下解釋:
1.抽象主題(Subject)

抽象主題角色把所有對觀察者對象的引用保存在一個(gè)聚集(比如ArrayList對象)里,每個(gè)主題都可以有任何數(shù)量的觀察者。抽象主題提供一個(gè)接口,可以增加和刪除觀察者對象,抽象主題角色又叫做抽象被觀察者(Observable)角色。

2.具體主題(ConcreteSubject)

將有關(guān)狀態(tài)存入具體觀察者對象;在具體主題的內(nèi)部狀態(tài)改變時(shí),給所有登記過的觀察者發(fā)出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。也就是觀察目標(biāo)哦。

3.抽象觀察者(Observer)

為所有的具體觀察者定義一個(gè)接口,在得到主題的通知時(shí)更新自己,這個(gè)接口叫做更新接口。

4.具體觀察者(ConcreteObserver)

具體觀察者角色實(shí)現(xiàn)抽象觀察者角色所要求的更新接口,以便使本身的狀態(tài)與主題的狀態(tài) 像協(xié)調(diào)。如果需要,具體觀察者角色可以保持一個(gè)指向具體主題對象的引用。

他們是一種一對多的關(guān)系,一個(gè)Subject(觀察目標(biāo))依賴多個(gè)Observer(觀察者),一個(gè)Observer依賴一個(gè)Subject。Subject需要做的是注冊觀察者,注銷觀察者,以及Subject改變時(shí)通知在線其他觀察者的方法。Observer定義了當(dāng)主題改變式,被通知時(shí)要調(diào)用的update方法。當(dāng)Observer被實(shí)例化的時(shí)候,會告訴調(diào)用Subject的registerObserver()注冊到Subject。當(dāng)Subject更新的時(shí)候會最終調(diào)用Observer的update方法通知到觀察者。

一、不使用觀察者模式的一個(gè)錯(cuò)誤案例:

public class WeatherData {
    //實(shí)例變量聲明
    private float temperaure;
    private float humidity;
    private float pressure;
    public  void measuremntsChanged(){
        float temp = getTemperaure();
        float  humidity = getHumidity();
        float pressure = getPressure();
        // 三種布告的實(shí)例對象,將從WeatherData中的數(shù)據(jù)傳入布告中。
        currentCOnditionDisplay.update(temp, humidity, pressure);
        statiscsDisplay.update(temp, humidity, pressure);
        forecastDispaly.update(temp, humidity, pressure);
    }
    // WeatherData的其他方法
    
    public float getTemperaure() {
        return temperaure;//溫度
    }
    public float getHumidity() {
        return humidity;//濕度
    }
    public float getPressure() {
        return pressure;//氣壓
    }
}

當(dāng)一個(gè)天氣主題發(fā)生變化時(shí),將調(diào)用measuremntsChanged得到變化后的數(shù)據(jù),再一個(gè)一個(gè)的通知其他需要改變的對象(比如目前的狀況、氣象統(tǒng)計(jì)、天氣預(yù)測)當(dāng)然,可能需要改變的對象會更多。當(dāng)不使用觀察者模式時(shí),需要通知的對象每個(gè)類都要有一個(gè)update方法,但正確的做法應(yīng)該抽取起來,封裝變化的部分。如果之后氣象站有變化,我們還有在具體的編程中增加或刪減代碼,這可能反而會引來其他的問題,是不當(dāng)?shù)牟僮鳌?/p>

二、使用觀察者:

針對上面的案例,如果我們使用觀察者模式,應(yīng)該怎么實(shí)現(xiàn)呢?

1.首先定義接口

觀察者接口

public interface Observer { 
    //觀察者中的更新數(shù)據(jù),當(dāng)數(shù)據(jù)更新時(shí)將會傳遞給觀察者
    public void update(float temp, float humidity, float pressure);
}

被觀察者接口

public interface Subject {
    //這兩個(gè)方法 需要一個(gè)觀察者作為參數(shù),用于注冊和刪除
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    // 當(dāng)主題改變時(shí),這個(gè)方法會被調(diào)用來通知所有的觀察者
    public void notifyObsevers();
}

2.編寫被觀察者對象 它需要實(shí)現(xiàn)被觀察者接口

public class WeatherData implements Subject {
    // 用數(shù)組來記錄觀察者
    private ArrayList observers;
    private float temperature;
    private float humdity;
    private float pressure;
    public WeatherData(){
        observers  = new ArrayList();
    }
    //當(dāng)注冊觀察者時(shí),我們將觀察者添加到數(shù)組中
    public void registerObserver(Observer o) {
            this.observers.add(o);
    }
    //當(dāng)觀察者想取消注冊,我們將觀察者從數(shù)組中刪除
    public void removeObserver(Observer o) {
        int i = observers.indexOf(0);
        if (i >= 0){
            observers.remove(o);
        }
    }
    //因?yàn)槊恳粋€(gè)觀察者都實(shí)現(xiàn)了update方法,所以我們在這里可以通知所有的觀察者
    public void notifyObsevers() {
        for (int i = 0; i < observers.size(); ++i){
            Observer observer = (Observer) observers.get(i);
            observer.update(temperature, humdity, pressure);
        }
    }
    //當(dāng)從氣象站更新觀測值時(shí),我們通知觀察者
    public void measurementChanged(){
        notifyObsevers();
    }
    //
    public void setMeassurements(float temperature, 
        float humdity, float pressure){
        this.temperature = temperature;
        this.pressure = pressure;
        this.humdity = humdity;
        measurementChanged();
    }
    // WeatherData的其他方法
}

3.編寫觀察者對象 它需要實(shí)現(xiàn)觀察者接口

因?yàn)橛^察者對象可能會有很多,每一個(gè)觀察者對象在目標(biāo)觀察對象更新后,都會作出不同的操作(在update中有自己獨(dú)特的邏輯),比如天氣狀況發(fā)生改變時(shí),當(dāng)前狀況這個(gè)對象需要更新天氣,預(yù)測天氣站需要實(shí)時(shí)預(yù)測,氣象統(tǒng)計(jì)又會做統(tǒng)計(jì)工作等等。

本文只寫一個(gè)模版為例:
當(dāng)前條件類CurrentWeatherCondition

public class CurrentWeatherCondition  implements Observer{
    private float temperature;
    private float humifity;
    private Subject weatherData;
    public CurrentWeatherCondition(Subject weatherData){
        /*
            這里為什么要保存Subject的引用呢?構(gòu)造完似乎用不著了呀?
                的確如此,但是我們可能以后想要取消注冊,如果已經(jīng)有了對
                Subject的引用會比較方便
         */
        this.weatherData = weatherData;
        // 注冊
        weatherData.registerObserver(this);
    }
    public void update(float temperature, 
        float humidity, float pressure) {
        this.temperature = temperature;
        this.humifity = humidity;
        //獲取數(shù)據(jù)后就在布告板中顯示。也就調(diào)用display方法
        display();
    }
    public void display() {
        //布告板中顯示就:直接簡單輸出
        Log.d("weather","Current conditions" 
            + temperature  + "F degrees " +
                "and " + humifity + "% humifity");
    }
}

至此我們便完成了第一個(gè)觀察者模式的例子。

三、觀察者模式的優(yōu)缺點(diǎn)

優(yōu)點(diǎn)
1.符合松耦合設(shè)計(jì)原則

對象之間要交互,暴露的東西越少越好。只需要知道對方能干嘛,而不需要知道具體怎么實(shí)現(xiàn)的。本例中。Observer不需要知道Subject怎么通知我的,只需要知道,通知我update方法被調(diào)用了即可。反過來。Subject不需要知道Observer具體是誰,做了什么。只需要知道它是個(gè)Observer即可。做到被觀察者和觀察者解耦

2.松耦合度的對象之間能夠維持行動的協(xié)調(diào)一致,保證高度的協(xié)作。
缺點(diǎn)
1.Java中消息的通知一般是順序執(zhí)行,那么一個(gè)觀察者卡頓,會影響整體的執(zhí)行效率,在這種情況下,一般會采用異步實(shí)現(xiàn)。
2.雖然觀察者模式可以隨時(shí)使觀察者知道所觀察的對象發(fā)生了變化,但是觀察者模式?jīng)]有相應(yīng)的機(jī)制使觀察者知道所觀察的對象是怎么發(fā)生變化的。
3.如果在被觀察者之間有循環(huán)依賴的話,被觀察者會觸發(fā)它們之間進(jìn)行循環(huán)調(diào)用,導(dǎo)致系統(tǒng)崩潰。在使用觀察者模式是要特別注意這一點(diǎn)。盡量避免。

四、JDK8內(nèi)置的觀察者模式

上述例子有一個(gè)弊端是:當(dāng)我們?nèi)U(kuò)展一個(gè)新的被觀察者Subject時(shí),我們會發(fā)現(xiàn)我們寫了重復(fù)的代碼。注冊刪除和通知Observer這些方法在每一個(gè)實(shí)現(xiàn)類當(dāng)中都需要去實(shí)現(xiàn)。關(guān)于這個(gè),使用jdk8的小伙伴們說很容易解決的啦。jdk8支持接口方法的默認(rèn)實(shí)現(xiàn)嘛,子類可以不實(shí)現(xiàn)這些方法。
Java API中有內(nèi)置的觀察者模式。java.util包中含有最基本的Observer接口與Observable類。源碼如下:

package java.util;

public interface Observer {
    void update(Observable var1, Object var2);
}

package java.util;

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

    public Observable() {
    }

    public synchronized void addObserver(Observer var1) {
        if (var1 == null) {
            throw new NullPointerException();
        } else {
            if (!this.obs.contains(var1)) {
                this.obs.addElement(var1);
            }

        }
    }

    public synchronized void deleteObserver(Observer var1) {
        this.obs.removeElement(var1);
    }

    public void notifyObservers() {
        this.notifyObservers((Object)null);
    }

    public void notifyObservers(Object var1) {
        Object[] var2;
        synchronized(this) {
            if (!this.changed) {
                return;
            }

            var2 = this.obs.toArray();
            this.clearChanged();
        }

        for(int var3 = var2.length - 1; var3 >= 0; --var3) {
            ((Observer)var2[var3]).update(this, var1);
        }

    }

    public synchronized void deleteObservers() {
        this.obs.removeAllElements();
    }

    protected synchronized void setChanged() {
        this.changed = true;
    }

    protected synchronized void clearChanged() {
        this.changed = false;
    }

    public synchronized boolean hasChanged() {
        return this.changed;
    }

    public synchronized int countObservers() {
        return this.obs.size();
    }
}

JDK定義Observable類為被觀察者 也就是我們上面的ConcreteSubject 具體被觀察者類,比起之前我們自己定義的Subject,多了一個(gè)狀態(tài)changed屬性,調(diào)用setChanged方法設(shè)置為true,調(diào)用notifyObservers方法的時(shí)候設(shè)置為false,changed的設(shè)計(jì)可以讓我們更有效的控制變化的通知頻率,更加靈活。觀察者是接口Observer。update方法接受一個(gè)具體被觀察者以及Object類型的參數(shù)。接受的參數(shù)就包含了依賴的被觀察者,第二個(gè)參數(shù)方便我們根據(jù)業(yè)務(wù)傳入其他的參數(shù)。

此時(shí)使用jdk8內(nèi)置的觀察者重新改造上面的代碼
1.編寫被觀察者對象 讓它繼承Observable類, 相比之前不需要實(shí)現(xiàn)被觀察者接口了

public class WeatherData extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;
   
    public WeatherData(){}
    public void measurementsChanged(){
        // 在調(diào)用notifyObservers()之前,要要先調(diào)用setChanged()來指示
        // 狀態(tài)已經(jīng)改變。
        setChanged();
        notifyObservers();
    }
    public void setMeasurements(float temperature, 
    float humidity, float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    public float getHumidity() {
        return humidity;
    }
    public float getPressure() {
        return pressure;
    }
    public float getTemperature() {
        return temperature;
    }
}

2.編寫觀察者對象,我們還是以上面的CurrentWeatherCondition為例

public class CurrentWeatherCondition implements Observer{
    private float temperature;
    private float humidity;
    Observable observable;
    public  CurrentWeatherCondition(Observable observable){
        this.observable = observable;
        observable.addObserver(this);
    }

    /*
       實(shí)現(xiàn) java.util.Observer接口中的update方法。
     */
    public void update(Observable obs, Object arg) {
   //判斷是否是自己想要觀察的觀察目標(biāo)
        if (obs instanceof WeatherData){
            WeatherData weatherData = (WeatherData) obs;
            this.temperature  = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            //獲取數(shù)據(jù)后就在布告板中顯示。也就調(diào)用display方法
        display();
        }
    }
    public void display() {
        //布告板中顯示就:直接簡單輸出
        Log.d("weather","Current conditions" 
            + temperature  + "F degrees " +
                "and " + humifity + "% humifity");
    }
}

如此一來,在使用的java內(nèi)置的觀察者接口之后,我們將上述的四個(gè)步驟簡化成了兩個(gè)。只需要編寫一個(gè)被觀察者類和一個(gè)觀察者類,同時(shí)當(dāng)擴(kuò)展新的被觀察者類也不用寫重復(fù)的注冊、移除、更新的相同代碼了。

五、思考

在日常使用中,其實(shí)很多點(diǎn)都使用的了觀察者模式,可以作為后續(xù)的思考。
1.日常每天見到的發(fā)布-訂閱(比如公眾號、新聞等)
2.android源碼中也有很多使用了觀察者模式:比如OnClickListener、ContentObserver、android.database.Observable、AdapternotifyDataSetChanged()等等;還有組件通訊庫RxJava、RxAndroid、EventBus。可以查閱源碼時(shí)注意一下。
3.例子:對設(shè)置 OnClickListener 來說, View 是被觀察者, OnClickListener 是觀察者,onClick()是事件,二者通過 setOnClickListener() 方法達(dá)成訂閱關(guān)系。訂閱之后用戶點(diǎn)擊按鈕的瞬間,Android Framework 就會將點(diǎn)擊事件發(fā)送給已經(jīng)注冊的 OnClickListener 的onClick中(因?yàn)関iew持有OnClickListener的引用)。采取這樣被動的觀察方式,既省去了反復(fù)檢索狀態(tài)的資源消耗,也能夠得到最高的反饋速度。
4.例子:rxjava 擴(kuò)展了觀察者模式,和view不同的是事件回調(diào)方式除了onClick()【onNext】即普通事件之外,增加了onerror()和oncomplete,如圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容