設計模式之【觀察者模式】
用最簡單的一句話來理解觀察者模式就是:當一個物件發生改變時,其相關依賴物件皆得到通知並被自動更新。

類圖
關於這個圖的四個物件有如下解釋:
1.抽象主題(Subject)
抽象主題角色把所有對觀察者物件的引用儲存在一個聚集(比如ArrayList物件)裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個介面,可以增加和刪除觀察者物件,抽象主題角色又叫做抽象被觀察者(Observable)角色。
2.具體主題(ConcreteSubject)
將有關狀態存入具體觀察者物件;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。也就是觀察目標哦。
3.抽象觀察者(Observer)
為所有的具體觀察者定義一個介面,在得到主題的通知時更新自己,這個介面叫做更新介面。
4.具體觀察者(ConcreteObserver)
具體觀察者角色實現抽象觀察者角色所要求的更新介面,以便使本身的狀態與主題的狀態 像協調。如果需要,具體觀察者角色可以保持一個指向具體主題物件的引用。
他們是一種一對多的關係,一個Subject(觀察目標)依賴多個Observer(觀察者),一個Observer依賴一個Subject。Subject需要做的是註冊觀察者,登出觀察者,以及Subject改變時通知線上其他觀察者的方法。Observer定義了當主題改變式,被通知時要呼叫的update方法。當Observer被例項化的時候,會告訴呼叫Subject的registerObserver()註冊到Subject。當Subject更新的時候會最終呼叫Observer的update方法通知到觀察者。
一、不使用觀察者模式的一個錯誤案例:
public class WeatherData { //例項變數宣告 private float temperaure; private float humidity; private float pressure; publicvoid measuremntsChanged(){ float temp = getTemperaure(); floathumidity = getHumidity(); float pressure = getPressure(); // 三種佈告的例項物件,將從WeatherData中的資料傳入佈告中。 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;//氣壓 } }
當一個天氣主題發生變化時,將呼叫measuremntsChanged得到變化後的資料,再一個一個的通知其他需要改變的物件(比如目前的狀況、氣象統計、天氣預測)當然,可能需要改變的物件會更多。當不使用觀察者模式時,需要通知的物件每個類都要有一個update方法,但正確的做法應該抽取起來,封裝變化的部分。如果之後氣象站有變化,我們還有在具體的程式設計中增加或刪減程式碼,這可能反而會引來其他的問題,是不當的操作。
二、使用觀察者:
針對上面的案例,如果我們使用觀察者模式,應該怎麼實現呢?
1.首先定義介面
觀察者介面
public interface Observer { //觀察者中的更新資料,當資料更新時將會傳遞給觀察者 public void update(float temp, float humidity, float pressure); }
被觀察者介面
public interface Subject { //這兩個方法 需要一個觀察者作為引數,用於註冊和刪除 public void registerObserver(Observer o); public void removeObserver(Observer o); // 當主題改變時,這個方法會被呼叫來通知所有的觀察者 public void notifyObsevers(); }
2.編寫被觀察者物件 它需要實現被觀察者介面
public class WeatherData implements Subject { // 用陣列來記錄觀察者 private ArrayList observers; private float temperature; private float humdity; private float pressure; public WeatherData(){ observers= new ArrayList(); } //當註冊觀察者時,我們將觀察者新增到陣列中 public void registerObserver(Observer o) { this.observers.add(o); } //當觀察者想取消註冊,我們將觀察者從陣列中刪除 public void removeObserver(Observer o) { int i = observers.indexOf(0); if (i >= 0){ observers.remove(o); } } //因為每一個觀察者都實現了update方法,所以我們在這裡可以通知所有的觀察者 public void notifyObsevers() { for (int i = 0; i < observers.size(); ++i){ Observer observer = (Observer) observers.get(i); observer.update(temperature, humdity, pressure); } } //當從氣象站更新觀測值時,我們通知觀察者 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.編寫觀察者物件 它需要實現觀察者介面
因為觀察者物件可能會有很多,每一個觀察者物件在目標觀察物件更新後,都會作出不同的操作(在update中有自己獨特的邏輯),比如天氣狀況發生改變時,當前狀況這個物件需要更新天氣,預測天氣站需要實時預測,氣象統計又會做統計工作等等。
本文只寫一個模版為例:
當前條件類CurrentWeatherCondition
public class CurrentWeatherConditionimplements Observer{ private float temperature; private float humifity; private Subject weatherData; public CurrentWeatherCondition(Subject weatherData){ /* 這裡為什麼要儲存Subject的引用呢?構造完似乎用不著了呀? 的確如此,但是我們可能以後想要取消註冊,如果已經有了對 Subject的引用會比較方便 */ this.weatherData = weatherData; // 註冊 weatherData.registerObserver(this); } public void update(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humifity = humidity; //獲取資料後就在佈告板中顯示。也就呼叫display方法 display(); } public void display() { //佈告板中顯示就:直接簡單輸出 Log.d("weather","Current conditions" + temperature+ "F degrees " + "and " + humifity + "% humifity"); } }
至此我們便完成了第一個觀察者模式的例子。
三、觀察者模式的優缺點
優點
1.符合鬆耦合設計原則
物件之間要互動,暴露的東西越少越好。只需要知道對方能幹嘛,而不需要知道具體怎麼實現的。本例中。Observer不需要知道Subject怎麼通知我的,只需要知道,通知我update方法被呼叫了即可。反過來。Subject不需要知道Observer具體是誰,做了什麼。只需要知道它是個Observer即可。做到 被觀察者和觀察者解耦
2.鬆耦合度的物件之間能夠維持行動的協調一致,保證高度的協作。
缺點
1.Java中訊息的通知一般是順序執行,那麼一個觀察者卡頓,會影響整體的執行效率,在這種情況下,一般會採用非同步實現。
2.雖然觀察者模式可以隨時使觀察者知道所觀察的物件發生了變化,但是觀察者模式沒有相應的機制使觀察者知道所觀察的物件是怎麼發生變化的。
3.如果在被觀察者之間有迴圈依賴的話,被觀察者會觸發它們之間進行迴圈呼叫,導致系統崩潰。在使用觀察者模式是要特別注意這一點。儘量避免。
四、JDK8內建的觀察者模式
上述例子有一個弊端是:當我們去擴充套件一個新的被觀察者Subject時,我們會發現我們寫了重複的程式碼。註冊刪除和通知Observer這些方法在每一個實現類當中都需要去實現。關於這個,使用jdk8的小夥伴們說很容易解決的啦。jdk8支援介面方法的預設實現嘛,子類可以不實現這些方法。
Java API中有內建的觀察者模式。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,多了一個狀態changed屬性,呼叫setChanged方法設定為true,呼叫notifyObservers方法的時候設定為false,changed的設計可以讓我們更有效的控制變化的通知頻率,更加靈活。觀察者是介面Observer。update方法接受一個具體被觀察者以及Object型別的引數。接受的引數就包含了依賴的被觀察者,第二個引數方便我們根據業務傳入其他的引數。
此時使用jdk8內建的觀察者重新改造上面的程式碼
1.編寫被觀察者物件 讓它繼承Observable類, 相比之前不需要實現被觀察者介面了
public class WeatherData extends Observable { private float temperature; private float humidity; private float pressure; public WeatherData(){} public void measurementsChanged(){ // 在呼叫notifyObservers()之前,要要先呼叫setChanged()來指示 // 狀態已經改變。 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; publicCurrentWeatherCondition(Observable observable){ this.observable = observable; observable.addObserver(this); } /* 實現 java.util.Observer介面中的update方法。 */ public void update(Observable obs, Object arg) { //判斷是否是自己想要觀察的觀察目標 if (obs instanceof WeatherData){ WeatherData weatherData = (WeatherData) obs; this.temperature= weatherData.getTemperature(); this.humidity = weatherData.getHumidity(); //獲取資料後就在佈告板中顯示。也就呼叫display方法 display(); } } public void display() { //佈告板中顯示就:直接簡單輸出 Log.d("weather","Current conditions" + temperature+ "F degrees " + "and " + humifity + "% humifity"); } }
如此一來,在使用的java內建的觀察者介面之後,我們將上述的四個步驟簡化成了兩個。只需要編寫一個被觀察者類和一個觀察者類,同時當擴充套件新的被觀察者類也不用寫重複的註冊、移除、更新的相同程式碼了。