下面做一個天氣預報系統,涉及到的參數一共有三種:temperature, humidity, pressure。外圍接受顯示裝置也分為三種:current condition display會列出當前的氣溫,濕度和氣壓;statistics display會列出當前的溫度最高,最低和平均值;forecast display列出將來預測的天氣狀況。現在我們設想用一個WeatherDataObject這個對象從氣象臺實時接受數據,再把更新的數據發送到外圍接受顯示裝置上去。如下:

我們先看右邊的三個顯示設備,我們把它跟前一章的Duck做比較:Duck是一個總稱,被做成抽象類,下面有許多各種各樣的duck,被做成該抽象類的子類,Duck的各種會變化的功能比如quackable和flyable被分別做成接口,每個接口下面有各種具體的功能實現它。同理,這個天氣情況顯示設備也好比是一個總稱,下面有三種顯示設備,而它的可變化的功能是display,因為未來可能再做第四種設備是用聲音報警的。。。
因為是實時接受和顯示數據,所以所有的設備必然包含update()功能,部分設備有display功能。UML如下:

public interface Observer
{
public void update(float temperature, float humidity, float pressure);
}
public interface Display
{
public void display();
}
public class CurrentCondition implements Observer, Display
{
private float temperature;
private float humidity;
public void display()
{
System.out.println("Current conditions: " + temperature + "F degrees and humidity " + humidity);
}
public void update(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
display();
}
}
public class Statistics implements Display, Observer
{
private static float maxT = 0;//歷史最高氣溫
private static float minT = 100;//歷史最低氣溫
private static float sumT = 0;//氣溫總和
private static int num = 0;//測量次數
public void display()
{
System.out.println("Max Temperature:"+maxT);
System.out.println("Min Temperature:"+minT);
System.out.println("Average Temperature:"+sumT/num);
}
public void update(float temperature, float humidity, float pressure)
{
if (temperature < minT)
{
minT = temperature;
}
if (temperature > maxT)
{
maxT = temperature;
}
sumT += temperature;
num++;
display();
}
}
public class Forecast implements Display, Observer
{
private static float currentPressure = 29;//設定一個當前氣壓的默認值,為簡化操作
private static float lastPressure = 29;//設定一個先前氣壓的默認值
public void display()
{
System.out.println("Forecast:");
if (currentPressure > lastPressure) //氣壓升高,天氣轉暖;氣壓降低,天氣轉涼
{
System.out.println("will be warmmer.");
}
else if (currentPressure == lastPressure)
{
System.out.println("will be the same");
}
else
{
System.out.println("will be cooler");
}
}
public void update(float temperature, float humidity, float pressure)
{
lastPressure = currentPressure;
currentPressure = pressure;
display();
}
}
現在著重考慮WeatherData 這個對象怎么寫,該對象首先要能夠從氣象站收取氣溫,濕度,氣壓的信息(通過氣象臺使用setMeasurements方法來設定WeatherData中的各屬性的值),并向終端顯示設備更新氣象信息(使用upadeDisplay方法),所以有:
public class WeatherData
{
private float temperature;
private float humidity;
private float pressure;
public void updateDisplay()
{
CurrentCondition currentCondition=new CurrentCondition();
Statistics statistics=new Statistics();
Forecast forecast=new Forecast();
currentCondition.update(temperature, humidity, pressure); //這部分是可變動的,可是并沒有被分離出去
statistics.update(temperature, humidity, pressure);
forecast.update(temperature, humidity, pressure);
}
public void setMeasurements(float termperature, float humidity, float pressure)
{
this.temperature=termperature;
this.humidity=humidity;
this.pressure=pressure;
updateDisplay();
}
}
最后我們用個主函數來調試一下:
public class Main
{
public static void main(String[] args)
{
WeatherData wd=new WeatherData();
wd.setMeasurements(20, 10, 15);
wd.setMeasurements(21, 11, 16);
}
}
輸出:
Current conditions: 20.0F degrees and humidity 10.0
Max Temperature:20.0
Min Temperature:20.0
Average Temperature:20.0
Forecast:
will be cooler
Current conditions: 21.0F degrees and humidity 11.0
Max Temperature:21.0
Min Temperature:20.0
Average Temperature:20.5
Forecast:
will be warmmer.
可以看出,一切運行正常,是不是這樣,該程序就搞定了呢?當然不是!當我們不想讓三個終端中的某一個接受天氣信息時,或者我們要增加一個新的終端來顯示其他的一些天氣信息時,我們勢必要改寫WeatherDate中的updateDisplay方法(上面源代碼中的紅字部分)。我們并沒有把WeatherDate中的不變化的setMeasurements方法和變化的updateDisplay方法分離開,也就是說沒有隔離出變化的部分,所以這個程序有待改進。
可不可以把updateDisplay寫成接口的形式分離出去呢?Good Idea! 不過updateDisplay之所以產生變化,不是像Duck的flyable那樣是功能性的變化,而是updateDisplay方法體本身中的執行過程需要增減。所以即使把updateDisplay作為接口分離出去,這個接口也要不斷的改變,多以我們對這類增減某一類似問題所產生的變化引入observer pattern。
所謂的observer pattern即是實現觀察者和觀察對象之間關系的一種模式。該模式中,觀察對象可以有一個,也可以有多個。如果觀察者要實時接受觀察對象的信息,就必須通過該觀察對象的registerObserver方法注冊成為該觀察對象的觀察者群。也可以通過removeObserver方法注銷并離開該觀察對象的觀察者群。觀察對象通過notifyObserver方法將實時信息通知給它所有的已注冊的觀察者。
observer pattern的本質實質是1對多的關系:每一個觀察對象可以對應著許多觀察者,而每一個觀察者又可以觀察許多觀察對象

把這種思路放到目前我們需要解決的氣象站的例子上去:

較前面的實現方式,主要變化的代碼如下:
public interface Subject
{
//之所以要寫一個接口而不直接使用WeatherData類,是因為該接口下面可能不止WeatherData這一個實現對象
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
import java.util.ArrayList;
public class WeatherData implements Subject
{
private float temperature;
private float humidity;
private float pressure;
private ArrayList<Observer> observers = new ArrayList<Observer>();
public WeatherData()
{
}
public void setMeasurement(float temperature, float humdity, float pressure)
{ //氣象站使用該方法設置新的氣象數值,并通知所有已注冊觀察者
this.temperature = temperature;
this.humidity = humdity;
this.pressure = pressure;
notifyObservers();
}
public void registerObserver(Observer o) //注冊觀察者
{
observers.add(o);
}
public void removeObserver(Observer o) //注銷觀察者
{
if (observers.indexOf(o) >= 0)
{
observers.remove(o);
}
}
public void notifyObservers() //通知所有的已注冊的觀察者新信息
{
for (Observer o : observers)
{
o.update(temperature, humidity, pressure);
}
}
}
public class Main
{
public static void main(String[] args)
{
WeatherData wd=new WeatherData();
CurrentCondition cc=new CurrentCondition();
wd.registerObserver(cc); //注冊CurrentCondition顯示方式到WeatherData的觀察者群中去
Statistics st=new Statistics();
wd.registerObserver(st); //同理,注冊Statistics的顯示方式
wd.setMeasurement(20, 10, 15);
wd.setMeasurement(21, 11, 16);
System.out.println("===================我是分割線=================");
wd.removeObserver(st); //注銷Statistics的顯示方式
wd.setMeasurement(19, 9, 14);
}
}
運行結果如下;
Current conditions: 20.0F degrees and humidity 10.0
Max Temperature:20.0
Min Temperature:20.0
Average Temperature:20.0
Current conditions: 21.0F degrees and humidity 11.0
Max Temperature:21.0
Min Temperature:20.0
Average Temperature:20.5
===================我是分割線=================
Current conditions: 19.0F degrees and humidity 9.0
由此,我們可以看到,我們能夠在Main方法中動態的注冊和注銷某被觀察對象的已注冊的觀察者,達到了我們預期的目的。
現在我們嘗試新增一個顯示方式HeatIndex,heat index是熱指數,它的計算方法為:heatIndex=16.923+1.85212*temperature+5.37941*humidity
很簡單,我們只要寫一個新的類,實現Observer和DisplayElement接口就可以了,在主程序Main中,我們再把這個顯示方式注冊到WeatherData的觀察者中去。

public class HeatIndex implements Observer, Display
{
private float temperature;
private float humidity;
public HeatIndex()
{
}
public void display()
{
float heatIndex = (float) (16.923 + 1.85212 * temperature + 5.37941 * humidity);
System.out.println("Heat Index is " + heatIndex);
}
public void update(float temperature, float humidity, float pressure)
{
this.humidity = humidity;
this.temperature = temperature;
display();
}
}
public class Main
{
public static void main(String[] args)
{
WeatherData wd = new WeatherData();
CurrentCondition cc = new CurrentCondition();
wd.registerObserver(cc);
Statistics st = new Statistics();
wd.registerObserver(st);
wd.setMeasurement(20, 10, 15);
wd.setMeasurement(21, 11, 16);
System.out.println("===================我是分割線=================");
wd.removeObserver(st);
HeatIndex hi = new HeatIndex();
wd.registerObserver(hi);
wd.setMeasurement(19, 9, 14);
}
}
運行結果如下:
Current conditions: 20.0F degrees and humidity 10.0
Max Temperature:20.0
Min Temperature:20.0
Average Temperature:20.0
Current conditions: 21.0F degrees and humidity 11.0
Max Temperature:21.0
Min Temperature:20.0
Average Temperature:20.5
===================我是分割線=================
Current conditions: 19.0F degrees and humidity 9.0
Heat Index is 100.52797
可見,我們很輕松的就添加了一個顯示方式,并動態的注冊到相應的觀察者中,而且最重要的是:我們沒有對源代碼做任何更改。