用最簡單的一句話來理解觀察者模式就是:當(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ù)的注冊、移除、更新的相同代碼了。