觀察者模式
參考《Head First設計模式》中的觀察者模式完成。
- 氣象監測應用需求
- 觀察者模式介紹
- 手寫觀察者模式
- Java内置的觀察者模式
氣象監測應用需求
根據氣象站實時輸出的濕度、溫度和氣壓值制作三塊布告闆。第一塊布告闆實時顯示目前的溫度、濕度和氣壓;第二塊布告闆顯示當日的平均溫度、最低溫度以及最高溫度;第三塊布告闆根據天氣顯示預報資訊。
氣象站提供了WeatherData類來獲得實時測量的溫度、濕度和氣壓值。
氣象站提供的接口如上如所示,三個getter方法用于擷取溫度、濕度和氣壓;每當氣象測量值變化,就會調用measurementsChanged()方法,實作measurementsChanged()就是我們的工作。
分析:
考慮到需求中布告闆顯示的内容可能會發生變化,為了友善日後修改程式,希望每次隻需要添加一塊新的布告闆,而其他程式不需要變化,努力做到互動對象之間松耦合。這個需求中,天氣測量值一旦變化,三個布告闆要及時獲得改變并展示,完全符合觀察者模式。
觀察者模式介紹
定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀态時,他的所有依賴者都會收到通知并自動更新。
觀察者模式中有主題和觀察者兩個對象,以報紙的訂閱為例簡述觀察者模式。
- 報社的任務就是出版報紙;
- 向某家報社訂閱報紙,隻要有新報紙出版,報社就會給你送來。隻要你訂閱了該報紙,就會一直收到新報紙;
- 當你不想看該報紙時,取消訂閱,他們就不會再送報紙來;
-
隻要報社還在營運,就一直有人向他們訂閱或取消訂閱報紙。
把訂閱報紙類比為觀察者模式,出版者就是“主題”,訂閱者就是“觀察者”。在該需求中,氣象站就是一個“主題”,而每塊布告闆就是一個“觀察者”。每個布告闆(觀察者)向氣象站(主題)注冊,就可以在測量值變化時獲得消息;取消某布告闆類比為“觀察者”向“主題”登出;新增一塊布告闆類比為新增一個氣象“主題”的“觀察者”。
手寫觀察者模式
面向對象的設計原則:針對接口程式設計,不針對實作程式設計。
根據上述原則,把主題和觀察者分别抽象為Subject和Observer接口。并添加一個DisplayElement接口用于展示。
Observer接口中隻有一個update()方法,用于當主題變化時執行。
Subject接口中有使得觀察者訂閱的方法registerObserver();當觀察者不想接收資訊時的取消訂閱方法removeObserver();以及當主題資料發生改變時通知訂閱該主題的所有觀察者的notifyObserver()方法。實際的主題,例如本需求中的WeatherData需要實作該接口,内部維護一個觀察者數組。registerObserver()方法中隻需要把觀察者加入自身的觀察者數組;removeObserver()方法中把要取消訂閱的觀察者移除;notifyObserver()則是周遊目前的觀察者數組,依次調用每個觀察者的update()即可。
Subject接口
/**
* Created by Janet on 2017/11/6.
* 觀察者模式中主題的接口
*/
public interface Subject {
public void registerObserver(Observer o);//觀察者o訂閱主題
public void removeObserver(Observer o);//觀察者o取消訂閱主題
public void notifyObserver();//主題通知觀察者
}
Observer接口
/**
* Created by Janet on 2017/11/6.
* 觀察者模式中觀察者的接口
*/
public interface Observer {
public void update(float temp,float humidity,float pressure);//主題傳來通知
}
用于展示的接口
/**
* Created by Janet on 2017/11/9.
* 展示結果的接口
*/
public interface DisplayElement {
public void display();
}
WeatherData主題
import java.util.ArrayList;
/**
* Created by Janet on 2017/11/6.
* 氣象預報的主題
*/
public class WeatherData implements Subject {
private ArrayList observers;//觀察者清單
private float temperature;//溫度
private float humidity;//濕度
private float pressure;//氣壓
//每次設定溫濕度和氣壓,主題都會發生改變
public void setTemperature(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public WeatherData(){
observers = new ArrayList();
}
//主題發生改變時執行的方法
public void measurementsChanged(){
notifyObserver();
}
//觀察者o訂閱主題
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
//觀察者o取消訂閱主題
@Override
public void removeObserver(Observer o) {
int index = observers.indexOf(o);
if( index >= ){
observers.remove(o);
}
}
//主題通知所有訂閱者
@Override
public void notifyObserver() {
for(int i = ;i<observers.size();i++){
Observer o = (Observer) observers.get(i);
o.update(temperature,humidity,pressure);
}
}
}
觀察者1–展示溫度濕度氣壓的布告闆
/**
* Created by Janet on 2017/11/6.
* 第一塊展示溫濕度的布告闆
*/
public class CurrentConditionDisplay implements Observer,DisplayElement {
private Subject weatherData;
private float temperature;
private float humidity;
//一建立就向主題注冊
public CurrentConditionDisplay(Subject weatherData){
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Current condition : "+temperature+"F degrees and "+humidity+"% humidity");
}
//主題變化時執行的函數
@Override
public void update(float temp, float humidity, float pressure) {
this.temperature = temp;
this.humidity = humidity;
display();
}
}
觀察者2–展示溫度最大最小及平均值的布告闆
/**
* Created by Janet on 2017/11/6.
* 第二塊展示最小,平均,最大溫度的布告闆
*/
public class StatisticsDisplay implements Observer,DisplayElement {
private float minTemperature = Float.MAX_VALUE;//最小溫度
private float avgTemperature;//平均溫度
private float maxTemperature = Float.MIN_VALUE;//最大溫度
private int num = ;//用于計算平均溫度
private Subject weatherData;
//建立觀察者時要向主題注冊
public StatisticsDisplay(Subject weatherData){
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Avg/Max/Min temperature = "+avgTemperature+"/"+maxTemperature+"/"+minTemperature);
}
//主題變化時執行的函數
@Override
public void update(float temp, float humidity, float pressure) {
if( minTemperature > temp ){
minTemperature = temp;
}
if( maxTemperature < temp ){
maxTemperature = temp;
}
avgTemperature = (avgTemperature * num + temp)/(num+);
num++;
display();
}
}
測試函數
/**
* Created by Janet on 2017/11/6.
*/
public class WeatherStation {
public static void main(String[] args){
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay o1 = new CurrentConditionDisplay(weatherData);//第一塊布告闆
StatisticsDisplay o2 = new StatisticsDisplay(weatherData);
weatherData.setTemperature(,,);
weatherData.setTemperature(,,);
weatherData.setTemperature(,,);
}
}
執行結果如下:
Java内置的觀察者模式
java.util包中的Observable是主題的超類(注意,java内置的主題是類不是接口),Observer是觀察者的接口。
實際主題需要繼承超類Observeable,已經寫好了觀察者訂閱,取消訂閱以及通知的方法。其中setChanged()方法用于當主題資料修改時,用來标記狀态已經改變。WeatherData内部的notifyObservers()方法會首先判斷标志位是否更改,再通知各觀察者。
觀察者擷取主題變化的資料實際上有“推”和“拉”兩種方式。“推”表示主題資料發生變化時,主動把變化的資料推送給訂閱的觀察者們;“拉”表示當觀察者需要時主動向主題索取資料。上文中我們隻是自己實作了主題“推資料”的方法。Java内置的觀察者模式支援“推”和“拉”兩種擷取資料的模式。
利用Java内置實作WeatherData
注意,使用Java内置主題時,notifyObservers()有兩種重載方法:
notifyObservers()和notifyObservers(Object arg);notifyObservers(Object arg)可以傳送指定資料給觀察者。notifyObservers()用于拉資料的模式,notifyObservers(Object arg)用于推的模式。
import java.util.Observable;
/**
* Created by Janet on 2017/11/9.
* 使用java内置類實作主題
*/
public class WeatherData extends Observable{//繼承Observable,内部實作了主題的建立觀察者清單等方法
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){}//此處無需自行建立觀察者清單,超類已經建立
public void setMeasurements(float temperature,float humidity,float pressure){
this.pressure = pressure;
this.humidity = humidity;
this.temperature = temperature;
measurementsChanged();//資料改變後調用該方法
}
private void measurementsChanged() {
setChanged();//設定改變标志位
notifyObservers();//沒有參數傳入,說明是觀察者向主題索取資料
}
public float getTemperature(){
return temperature;
}
public float getHumidity(){
return humidity;
}
public float getPressure(){
return pressure;
}
}
利用Java内置實作觀察者
注意,Java内置的觀察者擷取主題動态有“推”和“拉”兩種模式。推即為主題
import java.util.Observable;
import java.util.Observer;
/**
* Created by Janet on 2017/11/9.
* 使用java内置類實作觀察者
*/
public class CurrentConditionsDisplay implements Observer,DisplayElement {
private Observable observable;//觀察者内部記錄主題
private float temperature;
private float humidity;
//構造函數中把觀察者加入到主題中
public CurrentConditionsDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this);//主題把觀察者加入到清單
}
@Override
public void display() {
System.out.println("Current conditions: "+temperature+" F degrees and "+humidity+"% humidity");
}
//不同的觀察者索取的資料由update方法展示
@Override
public void update(Observable o, Object arg) {
if( o instanceof WeatherData ){
WeatherData weatherData = (WeatherData) o;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}
import java.util.Observable;
import java.util.Observer;
/**
* Created by Janet on 2017/11/9.
*/
public class StatisticsDisplay implements Observer,DisplayElement {
private Observable observable;
private float minTemperature = Float.MAX_VALUE;//最小溫度
private float avgTemperature;//平均溫度
private float maxTemperature = Float.MIN_VALUE;//最大溫度
private int num = ;//用于計算平均溫度
public StatisticsDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this);
}
@Override
public void display() {
System.out.println("Avg/Max/Min temperature = "+avgTemperature+"/"+maxTemperature+"/"+minTemperature);
}
@Override
public void update(Observable o, Object arg) {
if( o instanceof WeatherData ){
WeatherData weatherData = (WeatherData) o;
float temp = weatherData.getTemperature();
if( minTemperature > temp ){
minTemperature = temp;
}
if( maxTemperature < temp ){
maxTemperature = temp;
}
avgTemperature = (avgTemperature * num + temp)/(num+);
num++;
display();
}
}
}
import java.util.Observable;
import java.util.Observer;
/**
* Created by Janet on 2017/11/9.
*/
public class ForecastDisplay implements Observer,DisplayElement {
private Observable observable;//主題
private float currentPressure = f;
private float lastPressure;
public ForecastDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this);
}
@Override
public void display() {
if( this.currentPressure < this.lastPressure ){
System.out.println("氣壓變小");
}else if( this.currentPressure > this.lastPressure ){
System.out.println("氣壓變大");
}else{
System.out.println("氣壓不變");
}
}
@Override
public void update(Observable o, Object arg) {
if( o instanceof WeatherData ){
WeatherData weatherData = (WeatherData) o;
this.lastPressure = currentPressure;
this.currentPressure = weatherData.getPressure();
display();
}
}
}
測試函數
/**
* Created by Janet on 2017/11/9.
*/
public class WeatherStation {
public static void main(String[] args){
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasurements(,,);
weatherData.setMeasurements(,,);
weatherData.setMeasurements(,,);
}
}
運作結果如下:
Java内置和手寫觀察者模式的差別
- Java内置的主題采用類的形式,擴充性不如接口;
- Java内置觀察者建立的順序不等同于主題改變時通知的順序,在上例子中可見,而自寫的主題内部維護觀察者數組采用有序的ArrayList,可以保證順序。