在實現觀察者模式前,先看一個天氣預報系統的需求,WeatherData對象負責擷取天氣資料(溫度,濕度,壓力),然後顯示在公告板上,公告板可以有很多種類型。
不使用設計模式的做法:
public class WeatherData {//氣象測量更新資料時,此方法調用public void measurementsChanged() {//當前溫度float temp = getTemperature();//當前濕度float humidity = getHumidity();//當前壓力float pressure = getPressure();//更新公告板1currentConditionsDisplay.update(temp, humidity, pressure);//公告板2currentConditionsDisplay2.update(temp, humidity, pressure);//公告板3currentConditionsDisplay3.update(temp, humidity, pressure);.......}}
這種方法的缺點是顯而易見的:
針對具體實現編程而不是介面,每個公告板都要加入到WeatherData類中,如果有公告板的變動,需要更改代碼。
無法在運行時動態地增加/修改公告板
破壞了WeatherData的封裝
公告板的update方法可以抽象成一個公用介面
……
要解決這些問題,這時候使用觀察者模式就很方便了。
觀察者定義了對象間的一對多依賴,當一個對象(主題)狀態改變時,它的所有依賴者(觀察者)都會收到通知並且自動更新,觀察者移除後,不再收到通知。
松耦合:2個對象可以互動,但是不太清楚對方的細節,使得對象之間的互相依賴降到了最低
觀察者模式讓主題和觀察者之間松耦合。有新的觀察者出現時,主題的代碼不需要修改。觀察者在主題中註冊就行了,主題也不關心有多少觀察者,只是發送通知給所有實現了觀察者介面的對象。
觀察者模式的簡要類圖:
其中,主題Subjcet和觀察者Observer是1對多的關係
Subject介面的3個操作,registerObserver用來註冊觀察者對象,removeObserver用來移除觀察者對象,notifyObservers用來通知所有的觀察者。
Observer介面的update方法用於接收通知,其實就是用於被Subject實作類別的notifyObservers方法調用
現在看一下觀察者模式下天氣預報系統的實現:
WeatherData對象需要在資料變化時通知所有的公告板,根據上面的概念,WeatherData要實現Subjcet介面,公告板要實現Observer介面。
Subjcet介面:
public interface Subject {public void registerObserver(Observer o);public void removeObserver(Observer o);public void notifyObservers();}
Observer介面:
public interface Observer {public void update(float temp, float humidity, float pressure);}
WeatherData:
import java.util.ArrayList;/** * 擷取氣象資料,通知觀察者 * */public class WeatherData implements Subject {private ArrayList<Observer> observers;private float temperature;private float humidity;private float pressure;public WeatherData() {observers = new ArrayList<Observer>();}@Overridepublic void registerObserver(Observer o) {observers.add(o);}@Overridepublic void removeObserver(Observer o) {int i = observers.indexOf(o);if (i > 0)observers.remove(i);}@Overridepublic void notifyObservers() {for (int i = 0; i < observers.size(); i++) {Observer observer = observers.get(i);observer.update(temperature, humidity, pressure);}}public void measurementsChanged() {notifyObservers();}/** * 類比氣象資料改變 * */public void setMeasurements(float temperature, float humidity,float pressure) {this.temperature = temperature;this.humidity = humidity;this.pressure = pressure;measurementsChanged();}}
其中一種公告板CurrentConditionsDisplay:
/** * 公告板,其中一個觀察者 * */public class CurrentConditionsDisplay implements Observer, DisplayElement {private float temperature;private float humidity;private float pressure;//主題的引用private Subject weatherData;public CurrentConditionsDisplay(WeatherData weatherData) {this.weatherData = weatherData;weatherData.registerObserver(this);}@Overridepublic void display() {System.out.println("temperature="+temperature+";humidity="+humidity+";pressure="+pressure);}@Overridepublic void update(float temp, float humidity, float pressure) {this.temperature = temp;this.humidity = humidity;this.pressure = pressure;display();}//移除觀察者private void quit() {weatherData.removeObserver(this);}}
Displayment介面無關緊要,用於顯示資料而已
public interface DisplayElement {public void display();}
可以看到,實現Subject介面的WeatherData類中維護了一個Observer介面的Arraylist,以便動態維護註冊和取消的觀察者。
一旦天氣資料變化,調用measurementsChanged方法,measurementsChanged方法調用notifyObservers,notifyObservers其實就是遍曆在WeatherData類中註冊的所有Observer(公告板),然後調用它們的update方法。這是一個“推”的過程,Observer們不需要手動去Subject那裡取資料。這就實現了“當一個對象(主題)狀態改變時,它的所有依賴者(觀察者)都會收到通知並且自動更新,觀察者移除後,不再收到通知。”的概念
現在調用測試代碼:
public class WeatherStation {public static void main(String[] args) {WeatherData wData = new WeatherData();CurrentConditionsDisplay curDis = new CurrentConditionsDisplay(wData);wData.setMeasurements(80, 65, 30.4f);wData.setMeasurements(81, 61, 33.4f);wData.setMeasurements(82, 62, 31.4f);}}
控制台輸出:
temperature=80.0;humidity=65.0;pressure=30.4
temperature=81.0;humidity=61.0;pressure=33.4
temperature=82.0;humidity=62.0;pressure=31.4
JAVA內建的觀察者模式
JAVA中Observable類和Observer介面已經幫我們實現好了觀察者模式,而且可以選擇用“推”或者“拉”的方式。
推和拉的區別是觀察者是否主動去主題那邊取資料,這在代碼中是通過notifyObservers方法是否傳參體現的,如果傳參則是推,不傳參,更新的資料需要觀察者調用主題的get方法得到,這樣就是拉了。
改動以上的代碼,注釋部分顯示了改動的地方,特別要注意的是和之前的程式對比,它們實現的介面的變化!
WeatcherData.java
import java.util.HashMap;import java.util.Map;import java.util.Observable;/** * 擷取氣象資料,通知觀察者 * */public class WeatherData extends Observable {//implements Subject {//private ArrayList<Observer> observers;private float temperature;private float humidity;private float pressure;public WeatherData() {//observers = new ArrayList<Observer>();}//@Override//public void registerObserver(Observer o) {//observers.add(o);//}////@Override//public void removeObserver(Observer o) {//int i = observers.indexOf(o);//if (i > 0)//observers.remove(i);//}////@Override//public void notifyObservers() {//for (int i = 0; i < observers.size(); i++) {//Observer observer = observers.get(i);//observer.update(temperature, humidity, pressure);//}//}//public void measurementsChanged() {//notifyObservers();//}public void measurementsChanged() {/*notifyObservers是Observable內建的方法,使用之前要調用setchanged,觀察者才能收到通知*/setChanged();//現在把溫度等資訊封裝在map中傳給觀察者Map<String, Object> map = new HashMap<String, Object>();map.put("temperature", temperature);map.put("humidity", humidity);map.put("pressure", pressure);notifyObservers(map);}/** * 類比氣象資料改變 * */public void setMeasurements(float temperature, float humidity,float pressure) {this.temperature = temperature;this.humidity = humidity;this.pressure = pressure;measurementsChanged();}}
CurrentConditionsDisplay.java
import java.util.Map;import java.util.Observable;/** * 公告板,其中一個觀察者 * */public class CurrentConditionsDisplay implements DisplayElement, java.util.Observer{//Observer, DisplayElement {private float temperature;private float humidity;private float pressure;//主題的引用//private Subject weatherData;//Observable對象的引用private Observable observerable;//public CurrentConditionsDisplay(WeatherData weatherData) {//this.weatherData = weatherData;//weatherData.registerObserver(this);//}public CurrentConditionsDisplay(Observable observable) {this.observerable = observable;observerable.addObserver(this);}@Overridepublic void display() {System.out.println("temperature="+temperature+";humidity="+humidity+";pressure="+pressure);}//@Override//public void update(float temp, float humidity, float pressure) {//this.temperature = temp;//this.humidity = humidity;//this.pressure = pressure;//display();//}//使用java.util.Observer的update方法@Overridepublic void update(Observable o, Object arg) {//檢測是否是WeatherData這個主題傳遞的資訊if(o instanceof WeatherData) {//這是拉的方式//WeatherData weatherData = (WeatherData)o;//this.temperature = weatherData.getXXXX//這是推的方式Map<String, Object> map = (Map<String, Object>)arg;this.temperature = (float) map.get("temperature");this.humidity = (float) map.get("humidity");this.pressure = (float) map.get("pressure");display();}}}
測試類別不變,運行結果:
temperature=80.0;humidity=65.0;pressure=30.4
temperature=81.0;humidity=61.0;pressure=33.4
temperature=82.0;humidity=62.0;pressure=31.4
用JAVA內建的實現可以方便很多,但是也有缺點,比如Observable是一個類而不是像第一個程式那樣實現的Subjcet介面,這樣靈活性下降。
很多地方都用到了觀察者模式,比如Listener。
舉個例子,android的按鈕監聽
setOnClickListener(new OnClickListener(){
控制項對象其實是“主題”,OnClickListener類是觀察者,重寫其onClick方法相當於重寫update方法。按鈕被點擊時,註冊過的觀察者,即OnClickListener的“update方法”onCLick方法會被調用。