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