Java設計模式之觀察者模式

Java設計模式之觀察者模式

本文僅是個人觀點,如有錯誤請指正

簡介

當對象間存在一對多關系時,可以考慮使用觀察者模式(Observer Pattern)。比如,當一個對象被修改時,則會自動通知它的依賴對象。觀察者模式屬于行為型模式。

實現

接下來將用三個例子來說明觀察者模式,詳細代碼可以參考文末的地址。

需求

現在有一個氣象站,想對外發布數據,有一個公司知道后想要通過天氣站的數據建立一個天氣看板,這就交給了公司的開發小王,小王拿到需求后,很快就開始行動了,下面就是小王的實現方法

  • 氣象站類,主要提供溫度、壓力、濕度參數,以及數據更新,數據通知,再把需要接收參數的看板加進來指定通知對象。

    public class WeatherData {
    
        /**
         * 溫度
         */
        private float mTemperate;
    
        /**
         * 壓力
         */
        private float mPressure;
    
        /**
         * 濕度
         */
        private float mHumidity;
    
        private CurrentConditions mCurrentConditions;
    
        /**
         * 加入需要通知的看板
         * @param mCurrentConditions
         */
        public WeatherData(CurrentConditions mCurrentConditions) {
            this.mCurrentConditions = mCurrentConditions;
        }
    
        public float getTemperature() {
            return mTemperate;
    
        }
    
        public float getPressure() {
            return mPressure;
    
        }
    
        public float getHumidity() {
            return mHumidity;
    
        }
    
        /**
         * 通知數據修改
         */
        public void dataChange() {
            mCurrentConditions.update(getTemperature(), getPressure(), getHumidity());
        }
    
        /**
         * 數據修改 同時調用通知方法
         *
         * @param mTemperature
         * @param mPressure
         * @param mHumidity
         */
        public void setData(float mTemperature, float mPressure, float mHumidity) {
            this.mTemperate = mTemperature;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
            dataChange();
        }
    
    }
    
  • 然后就是溫度看板,獲取當前的溫度,兩個方法,一個接收數據更新,一個展示數據。

    public class CurrentConditions {
    
        private float mTemperature;
        private float mPressure;
        private float mHumidity;
    
        public void update(float mTemperature, float mPressure, float mHumidity) {
            this.mTemperature = mTemperature;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
            display();
        }
    
        public void display() {
            System.out.println("***Today mTemperature: " + mTemperature + "***");
            System.out.println("***Today mPressure: " + mPressure + "***");
            System.out.println("***Today mHumidity: " + mHumidity + "***");
        }
    }
    
  • 好了咱們開始測試一下看看效果如何

    public class InternetWeather {
    
        public static void main(String[] args) {
            CurrentConditions mCurrentConditions = new CurrentConditions();
            WeatherData mWeatherData = new WeatherData(mCurrentConditions);
            mWeatherData.setData(30, 150, 40);
        }
    }
    
    
  • 執行結果

    ***Today mTemperature: 30.0***
    ***Today mPressure: 150.0***
    ***Today mHumidity: 40.0***
    

好了看起來需求已經滿足了,開始使用吧,然而沒過多久,另外一個公司也和天氣站聯系上了,想要他們提供的天氣數據來展示明天的天氣,小王一聽便說那還不簡單,去weather類里面加上你的看板類,加上通知,加上。。

不對啊,這加一個使用者就這么麻煩了,那以后要是來了更多的使用者怎么辦?又或者有的使用者突然又不使用這個數據了,想要取消通知怎么辦?要是按這樣的話改來改去每次修改都要重新上線,會非常麻煩,數據的及時性也得不到保證。小王頭疼不已,這時候公司里的老李看到了,笑呵呵的說小王,不要著急,我們再重新看看你寫的方法,再想想需要加入的接口和你有什么共同的地方,咱們把它抽離出去,然后再。。。。。小王在老李的幫助下很快做出了第二版實現

  • 首先我們加入數據提供方接口,將我們之前提到了的,新增需求方,刪除需求方,以及通知加入進去

    public interface Subject {
      /**
       * 注冊觀察者
       * @param o
       */
      void registerObserver(Observer o);
    
      /**
       * 移除觀察者
       * @param o
       */
      void removeObserver(Observer o);
    
      /**
       * 通知觀察者
       */
      void notifyObservers();
    }
    
  • 接著就是需求方的接口,主要用來獲取數據

    public interface Observer {
    
        /**
         * 數據更新
         * @param mTemperate
         * @param mPressure
         * @param mHumidity
         */
        void update(float mTemperate, float mPressure, float mHumidity);
    }
    
  • 接著修改之前的天氣數據類,在這里面我們實現前面的數據提供方的接口,然后重寫里面的方法,注冊接口我們暫時使用list作為存儲觀察者的集合,移除接口就是移除集合中的觀察者,這里我們檢查一下觀察者是否存在集合中再移除,然后是通知接口,我們遍歷集合逐個通知需求方。(這里我們是將數據一個一個的傳給使用方)。

    public class WeatherDataSt implements Subject {
    
        private float mTemperate;
        private float mPressure;
        private float mHumidity;
        private List<Observer> mObservers;
    
        public WeatherDataSt() {
            mObservers = new ArrayList<>();
        }
    
        public float getTemperature() {
            return mTemperate;
    
        }
    
        public float getPressure() {
            return mPressure;
    
        }
    
        public float getHumidity() {
            return mHumidity;
    
        }
    
        public void dataChange() {
            notifyObservers();
        }
    
    
        public void setData(float mTemperate, float mPressure, float mHumidity) {
            this.mTemperate = mTemperate;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
            dataChange();
        }
    
        @Override
        public void registerObserver(Observer o) {
            mObservers.add(o);
        }
    
        @Override
        public void removeObserver(Observer observer) {
            if (mObservers.contains(observer)) {
                mObservers.remove(observer);
            }
        }
    
        @Override
        public void notifyObservers() {
            for (Observer mObserver : mObservers) {
                mObserver.update(getTemperature(), getPressure(), getHumidity());
                System.out.println("開始通知:" + mObserver.getClass().getSimpleName());
            }
        }
    
    }
    
  • 天氣看板中實現觀察者接口,實現當中的數據接口

    public class CurrentConditions implements Observer {
        private float mTemperate;
        private float mPressure;
        private float mHumidity;
    
        @Override
        public void update(float mTemperate, float mPressure, float mHumidity) {
            this.mHumidity = mHumidity;
            this.mPressure = mPressure;
            this.mTemperate = mTemperate;
            display();
        }
    
        public void display() {
            System.out.println("***Today mTemperate:" + mTemperate + "***");
            System.out.println("***Today mPressure:" + mPressure + "***");
            System.out.println("***Today mHumidity:" + mHumidity + "***");
        }
    
    }
    
  • 在查看明天的天氣面板實現觀察者接口

    public class ForcesConditions implements Observer {
        private float mTemperate;
        private float mPressure;
        private float mHumidity;
    
        @Override
        public void update(float mTemperate, float mPressure, float mHumidity) {
            this.mTemperate = mTemperate;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
            display();
        }
    
        public void display() {
            System.out.println("**明天溫度:" + (mTemperate + Math.random()) + "**");
            System.out.println("**明天氣壓:" + (mPressure + 10 * Math.random()) + "**");
            System.out.println("**明天濕度:" + (mHumidity + Math.random()) + "**");
        }
    }
    
    
  • 然后我們編寫測試類

    public class InternetWeather {
    
        public static void main(String[] args) {
    
            CurrentConditions mCurrentConditions= new CurrentConditions();
            ForcesConditions mForcesConditions = new ForcesConditions();
            WeatherDataSt mWeatherDataSt = new WeatherDataSt();
    
            //注冊天氣看板
            mWeatherDataSt.registerObserver(mCurrentConditions);
            mWeatherDataSt.registerObserver(mForcesConditions);
    
            mWeatherDataSt.setData(30, 150, 40);
            //移除看板
            mWeatherDataSt.removeObserver(mCurrentConditions);
            mWeatherDataSt.setData(40, 250, 50);
        }
    
    }
    
    
  • 打印結果

    ***Today mTemperate:30.0***
    ***Today mPressure:150.0***
    ***Today mHumidity:40.0***
    開始通知:CurrentConditions
    **明天溫度:30.708855560149452**
    **明天氣壓:156.09944964615488**
    **明天濕度:40.252322896560706**
    開始通知:ForcesConditions
    **明天溫度:40.194891069978894**
    **明天氣壓:253.89775138044408**
    **明天濕度:50.95728186211783**
    開始通知:ForcesConditions
    

我們看到經過老李的一番指導,現在的設計基本上滿足了需求,即使新增看板繼續繼承觀察者接口,不需要的觀察者就可以直接移除,這樣提高了擴展性,去除了之前代碼每次新增需要修改代碼的弊端,實現了開閉原則。

接著老李又說,小王啊,其實java內置對象中就已經實現了觀察者模式我們只需要拿來用就可以了。。。

怎么用呢?請看下面的實現。

  • 天氣數據,這里我們不再實現subject接口,反而繼承java內置對象Observable,在內置對象中已經幫我們實現了通知,注冊,移除等方法,所以我們只需要寫我們自定義的方法就行了。

    public class WeatherData extends Observable {
        private float mTemperate;
        private float mPressure;
        private float mHumidity;
    
        public float getTemperature() {
            return mTemperate;
    
        }
    
        public float getPressure() {
            return mPressure;
    
        }
    
        public float getHumidity() {
            return mHumidity;
    
        }
    
        /**
         * 通知數據已修改
         */
        public void dataChange() {
            //Observable的源碼中setChanged(),設置changed為true表示數據已經修改,只有當changed為true的時候notifyObservers()方法才會通知修改
            this.setChanged();
            this.notifyObservers(new AssembleWeatherData(getTemperature(), getPressure(), getHumidity()));
    
        }
    
        /**
         * 修改數據
         * @param mTemperate
         * @param mPressure
         * @param mHumidity
         */
        public void setData(float mTemperate, float mPressure, float mHumidity) {
            this.mTemperate = mTemperate;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
            dataChange();
        }
    
    }
    
  • 同樣天氣看板類只用實現Observer接口,重寫當中的update方法就行了

    public class CurrentConditions implements Observer {
    
        private float mTemperate;
        private float mPressure;
        private float mHumidity;
    
        @Override
        public void update(Observable arg0, Object arg1) {
            this.mTemperate = ((AssembleWeatherData) (arg1)).mTemperate;
            this.mPressure = ((AssembleWeatherData) (arg1)).mPressure;
            this.mHumidity = ((AssembleWeatherData) (arg1)).mHumidity;
            display();
        }
    
        public void display() {
            System.out.println("***Today mTemperate:" + mTemperate + "***");
            System.out.println("***Today mPressure:" + mPressure + "***");
            System.out.println("***Today mHumidity:" + mHumidity + "***");
        }
    }
    
  • 另外一個天氣看板,一樣的實現

    public class ForecastConditions implements Observer {
    
        private float mTemperate;
        private float mPressure;
        private float mHumidity;
    
        /**
         * 數據更新 同時調用展示數據方法
         *
         * @param arg0
         * @param arg1
         */
        @Override
        public void update(Observable arg0, Object arg1) {
            this.mTemperate = ((AssembleWeatherData) (arg1)).mTemperate;
            this.mPressure = ((AssembleWeatherData) (arg1)).mPressure;
            this.mHumidity = ((AssembleWeatherData) (arg1)).mHumidity;
            display();
        }
    
        /**
         * 展示數據
         */
        public void display() {
            System.out.println("**明天溫度:" + (mTemperate + Math.random()) + "**");
            System.out.println("**明天氣壓:" + (mPressure + 10 * Math.random()) + "**");
            System.out.println("**明天濕度:" + (mHumidity + Math.random()) + "**");
        }
    
    
    }
    
  • 為了方便傳遞數據,我們再新建一個AssembleWeatherData類,用于設計返回數據內容

    public class AssembleWeatherData {
        public float mTemperate;
        public float mPressure;
        public float mHumidity;
    
        public AssembleWeatherData(float mTemperate, float mPressure, float mHumidity) {
            this.mTemperate = mTemperate;
            this.mPressure = mPressure;
            this.mHumidity = mHumidity;
        }
    }
    
  • 接著我們新建測試類

    public class InternetWeather {
        
        public static void main(String[] args) {
            CurrentConditions mCurrentConditions;
            ForecastConditions mForecastConditions;
            WeatherData mWeatherData;
    
            mCurrentConditions = new CurrentConditions();
            mForecastConditions = new ForecastConditions();
            mWeatherData = new WeatherData();
    
            //java內置觀察者模式中的addObserver方法,用于注冊觀察者
            mWeatherData.addObserver(mCurrentConditions);
            mWeatherData.addObserver(mForecastConditions);
            mWeatherData.setData(30, 150, 40);
    
            //內置方法,用于移除觀察者,重新修改數據
            mWeatherData.deleteObserver(mCurrentConditions);
            mWeatherData.setData(35, 150, 60);
    
        }
    }
    
  • 打印數據

    **明天溫度:30.676433372455936**
    **明天氣壓:154.2686999140749**
    **明天濕度:40.015042461544155**
    ***Today mTemperate:30.0***
    ***Today mPressure:150.0***
    ***Today mHumidity:40.0***
    **明天溫度:35.4090594721898**
    **明天氣壓:157.28459919820716**
    **明天濕度:60.286781430877525**
    

果然經過老李一番重構,小王很快就明白了,觀察者模式的好處,看著老李地中海的腦袋心想,這或許就是成為大牛的代價吧。。。。

總結

前面講述了兩種觀察者模式的實現方法,一個是我們自己寫的接口做的實現,另一種是直接使用java中的內置觀察者模式實現,兩者各有各的優勢和缺點,對于一般的情況內置觀察者就能滿足,但是有一點Observable是一個類不是接口所以只能繼承他,但由于java只能對接口多繼承,所以使用內置對象的時候需要多繼承的時候就不能使用了,自定義的方法可以更靈活,但是需要寫更多的代碼,魚和熊掌不可兼得。

鏈接

本文代碼倉庫地址:https://gitee.com/singlekingdom/JavaDesignPatterns.git

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容