一、『觀察者模式』定義
定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的所有依賴者都會收到通知並自動更新。
可以用【出版者+訂閱者=觀察者模式】的方式來類比。
如果你瞭解報紙的訂閱是怎麼回事,其實就知道觀察者模式是怎麼回事,只是名稱不太一樣:出版者改稱爲【主題】(Subject),訂閱者改稱爲【觀察者】(Observer)。用戶可以申請成爲訂閱者,也可以取消;而一旦出版者有新的報紙,訂閱者就可以收到。
這裏出版者只有一個,而訂閱者可以有多個,因此【主題】和【觀察者】定義了一對多的關係。【觀察者】依賴於此【主題】,只要【主題】狀態一有變化,【觀察者】就會被通知。根據通知的風格,【觀察者】可能因新值而更新。
二、場景
下面還是通過具體場景來一步步說明。
場景:某系統中有三個部分,分別是氣象站(獲取實際氣象數據的物理裝置)、WeatherData對象(追蹤來自氣象站的數據,並更新佈告板)和佈告板(顯示目前天氣狀況給用戶看)。
氣象站有很多感應裝置,比如溼度感應裝置、溫度感應裝置和氣壓感應裝置。WeatherData對象只有一個,而佈告板有多個,我們想要WeatherData對象把數據傳給所有需要數據的佈告板。
2.1 自定義觀察者模式
首先需要定義【主題】和【觀察者】接口,再讓WeatherData對象實現【主題】接口,佈告板實現【觀察者】接口。
/**
* 主題接口
*/
interface Subject {
// 註冊觀察者
public void registerObserver(Observer observer);
// 刪除觀察者
public void removeObserver(Observer observer);
// 主題改變,通知所有觀察者
public void notifyObserver();
}
/**
* 觀察者接口
*/
interface Observer {
public void update(float temperature, float humidity, float pressure);
}
/**
* WeatherData對象(實現主題接口),只有一個
*/
class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
public void registerObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
int i = observers.indexOf(observer);
if (i >= 0) {
observers.remove(i);
}
}
public void notifyObserver() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void measurementsChanged() {
notifyObserver();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
/**
* 公告板1(實現觀察者接口),可以有多個不同的公告板
*/
class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
private float pressure;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this); // 註冊
}
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
System.out.println("temperature=" + temperature +
", humidity=" + humidity +
", pressure=" + pressure);
}
}
/**
* 測試
*/
public class WeatherStation {
public static void main(String[] args) {
// 主題
WeatherData weatherData = new WeatherData();
// 第一個觀察者
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
// 多次通知
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
觀察者模式有兩種方式,分別是推(push)和拉(pull)方式。
-
推(push)方式,即【主題】把數據全部推給【觀察者】。上面的代碼就是採用推方式。
-
拉(pull)方式,即【觀察者】從【主題】拉取自己需要的數據。拉方式只需要在推方式的基礎上在【主題】里加上getter方法,讓【觀察者】可以通過調用getter方法獲取數據。
2.2 Java內置觀察者模式
Java API有內置的觀察者模式。java.util包內包含最基本的Observer接口與Observable類,這和我們的Subject接口與Observer接口很相似。內置的Observer接口與Observable類使用上更方便,因爲許多功能都已經事先準備好了。你也可以使用推(push)或拉(pull)的方式傳送數據。
2.2.1 內置觀察者模式的源代碼
Observale類(可觀察者,即【主題】)jdk1.8源代碼:
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
// 臨時數組緩衝區,用作當前觀察者狀態的快照。
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
public synchronized void deleteObservers() {
obs.removeAllElements();
}
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
public synchronized int countObservers() {
return obs.size();
}
}
有些時候【主題】改變了,但我們並不想通知【觀察者】,這時就可以使用changed標誌來進行判斷。需要通知時就調用setChanged()方法,把changed設爲true。不想通知時就調用clearChanged()方法,將changed狀態設置回false。另外也有一個hasChanged()方法,告訴你changed標誌的當前狀態。
Observer接口(即【觀察者】)jdk1.8源代碼:
public interface Observer {
void update(Observable o, Object arg);
}
2.2.2 使用內置觀察者模式
import java.util.Observable;
import java.util.Observer;
/**
* WeatherData對象(繼承內置被觀察者類Observale),只有一個
*/
class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
}
public void measurementsChanged() {
setChanged();
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
/**
* 公告板1(實現觀察者接口),可以有多個不同的公告板
*/
class CurrentConditionsDisplay implements Observer {
Observable observable;
private float temperature;
private float humidity;
private float pressure;
public CurrentConditionsDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this); // 註冊
}
public void update(Observable observable, Object arg) {
if (observable instanceof WeatherData) {
WeatherData weatherData = (WeatherData) observable;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
this.pressure = weatherData.getPressure();
display();
}
}
public void display() {
System.out.println("temperature=" + temperature +
", humidity=" + humidity +
", pressure=" + pressure);
}
}
/**
* 測試
*/
public class WeatherStation {
public static void main(String[] args) {
// 主題
WeatherData weatherData = new WeatherData();
// 第一個觀察者
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
// 多次通知
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
這裏採用的是拉方式。
三、總結
觀察者模式提供了一種對象設計,讓主題和觀察者之間松耦合。
關於觀察者的一切,主題只知道觀察者實現了某個接口(也就是Observer接口)。主題不需要知道觀察者的具體類是誰、做了些什麼或其他任何細節。
任何時候我們都可以增加新的觀察者。因爲主題唯一依賴的東西是一個實現Observer接口的對象列表,所以我們可以隨時增加觀察者。事實上,在運行時我們可以用新的觀察者取代現有的觀察者,主題不會受到任何影響。同樣的,也可以在任何時候刪除某些觀察者。
有新類型的觀察者出現時,主題的代碼不需要修改。假如我們有個新的具體類需要當觀察者,我們不需要爲了兼容新類型而修改主題的代碼,所有要做的就是在新的類裏實現此觀察者接口,然後註冊爲觀察者即可。主題不在乎別的,它只會發送通知給所有實現了觀察者接口的對象。
我們可以獨立地複用主題或觀察者。如果我們在其他地方需要使用主題或觀察者,可以輕易地複用,因爲二者並非緊耦合。
改變主題或觀察者其中一方,並不會影響另一方。因爲兩者是松耦合的,所以只要他們之間的接口仍被遵守,我們就可以自由地改變他們。
設計原則:爲了交互對象之間的松耦合設計而努力。
要點:
- 觀察者模式定義了對象之間一對多的關係。
- 主題(也就是可觀察者)用一個共同的接口來更新觀察者。
- 觀察者和可觀察者之間用松耦合方式結合(loosecoupl-ing),可觀察者不知道觀察者的細節,只知道觀察者實現了觀察者接口。
- 使用此模式時,你可從被觀察者處推(push)或拉(pull)數據(然而,推的方式被認爲更“正確”)。
- 有多個觀察者時,不可以依賴特定的通知次序。
- Java有多種觀察者模式的實現,包括了通用的java.util.Observable。
- 要注意java.util.Observable實現上所帶來的一些問題。比如Observable是一個類,Observable將關鍵的方法保護起來。
參考
《HeadFirst設計模式》