正文
一、定義
觀察者模式定義了物件之間的一對多依賴,這樣一來,當一個物件改變狀態時,它的所有依賴者都會收到通知並自動更新。
要點:
- 觀察者模式定義了物件之間一對多的關係。
- 觀察者模式讓主題(可觀察者)和觀察者之間鬆耦合。
- 主題物件管理某些資料,當主題內的資料改變時,會以某種形式通知觀察者。
- 觀察者可以訂閱(註冊)主題,以便在主題資料改變時能收到更新。
- 觀察者如果不想收到主題的更新通知,可以隨時取消訂閱(註冊)。
二、實現步驟
1、建立主題父類/介面
主題父類/介面主要提供了註冊觀察者、移除觀察者、通知觀察者三個方法。
/**
* 主題
*/
public class Subject {
/**
* 觀察者列表
*/
private ArrayList<Observer> observers;
public Subject() {
observers = new ArrayList<>();
}
/**
* 註冊觀察者
*/
public void registerObserver(Observer o) {
observers.add(o);
}
/**
* 移除觀察者
*/
public void removeObserver(Observer o) {
observers.remove(o);
}
/**
* 通知所有觀察者,並推送資料(也可以不推送資料,而是由觀察者過來拉取資料)
*/
public void notifyObservers(Object data) {
for (Observer o : observers) {
o.update(data);
}
}
}
2、建立觀察者介面
觀察者介面主要提供了更新方法,以供主題通知觀察者時呼叫。
/**
* 觀察者介面
*/
public interface Observer {
/**
* 根據主題推送的資料進行更新操作
*/
public void update(Object data);
}
3、建立具體的主題,並繼承主題父類/實現主題介面
/**
* 主題A
*/
public class SubjectA extends Subject {
/**
* 主題資料
*/
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
// 資料發生變化時,通知觀察者
notifyObservers(data);
}
}
4、建立具體的觀察者,並實現觀察者介面
通過觀察者類的建構函式,註冊成為主題的觀察者。
(1)觀察者 A
/**
* 觀察者A
*/
public class ObserverImplA implements Observer {
private Subject subject;
public ObserverImplA(Subject subject) {
// 儲存主題引用,以便後續取消註冊
this.subject = subject;
// 註冊觀察者
subject.registerObserver(this);
}
@Override
public void update(Object data) {
System.out.println("Observer A:" + data.toString());
}
}
(2)觀察者 B
/**
* 觀察者B
*/
public class ObserverImplB implements Observer {
private Subject subject;
public ObserverImplB(Subject subject) {
// 儲存主題引用,以便後續取消註冊
this.subject = subject;
// 註冊觀察者
subject.registerObserver(this);
}
@Override
public void update(Object data) {
System.out.println("Observer B:" + data.toString());
}
}
5、使用主題和觀察者物件
public class Test {
public static void main(String[] args) {
// 主題
SubjectA subject = new SubjectA();
// 觀察者A
ObserverImplA observerA = new ObserverImplA(subject);
// 觀察者B
ObserverImplB observerB = new ObserverImplB(subject);
// 模擬主題資料變化
subject.setData("I'm Batman!!!");
subject.setData("Why so serious...");
}
}
三、舉個例子
1、背景
你的團隊剛剛贏得一紙合約,負責建立 Weather-O-Rama 公司的下一代氣象站——Internet 氣象觀測站。
該氣象站建立在 WeatherData 物件上,由 WeatherData 物件負責追蹤目前的天氣狀況(溫度、溼度、氣壓)。並且具有三種佈告板,分別顯示目前的狀況、氣象統計以及簡單的預報。當 WeatherData 物件獲得最新的測量資料時,三種佈告板必須實時更新。
並且,這是一個可擴充套件的氣象站,Weather-O-Rama 氣象站希望公佈一組 API,好讓其他開發人員可以寫出自己的氣象佈告板,並插入此應用中。
2、實現
(1)建立主題父類
/**
* 主題
*/
public class Subject {
/**
* 觀察者列表
*/
private ArrayList<Observer> observers;
public Subject() {
observers = new ArrayList<>();
}
/**
* 註冊觀察者
*/
public void registerObserver(Observer o) {
observers.add(o);
}
/**
* 移除觀察者
*/
public void removeObserver(Observer o) {
observers.remove(o);
}
/**
* 通知所有觀察者,並推送資料
*/
public void notifyObservers(float temperature, float humidity, float pressure) {
for (Observer o : observers) {
o.update(temperature, humidity, pressure);
}
}
}
(2)建立觀察者介面
/**
* 觀察者介面
*/
public interface Observer {
/**
* 更新觀測值
*/
public void update(float temperature, float humidity, float pressure);
}
(3)建立氣象資料類,並繼承主題父類
/**
* 氣象資料
*/
public class WeatherData extends Subject {
/**
* 溫度
*/
private float temperature;
/**
* 溼度
*/
private float humidity;
/**
* 氣壓
*/
private float pressure;
public void measurementsChanged() {
// 觀測值變化時,通知所有觀察者
notifyObservers(temperature, humidity, pressure);
}
/**
* 設定觀測值(模擬觀測值變化)
*/
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
(4)建立佈告板,並實現觀察者介面
/**
* 目前狀態佈告板
*/
public class CurrentConditionsDisplay implements Observer {
private Subject weatherData;
private float temperature;
private float humidity;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
// 註冊觀察者
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display() {
System.out.println("Current conditions:" + temperature + "F degress and " + humidity + "% humidity");
}
}
/**
* 統計佈告板
*/
public class StatisticsDisplay implements Observer {
private Subject weatherData;
private ArrayList<Float> historyTemperatures;
public StatisticsDisplay(Subject weatherData) {
this.weatherData = weatherData;
// 註冊觀察者
weatherData.registerObserver(this);
historyTemperatures = new ArrayList<>();
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.historyTemperatures.add(temperature);
display();
}
public void display() {
if (historyTemperatures.isEmpty()) {
return;
}
Collections.sort(historyTemperatures);
float avgTemperature = 0;
float maxTemperature = historyTemperatures.get(historyTemperatures.size() - 1);
float minTemperature = historyTemperatures.get(0);
float totalTemperature = 0;
for (Float temperature : historyTemperatures) {
totalTemperature += temperature;
}
avgTemperature = totalTemperature / historyTemperatures.size();
System.out.println("Avg/Max/Min temperature:" + avgTemperature + "/" + maxTemperature + "/" + minTemperature);
}
}
/**
* 預測佈告板
*/
public class ForecastDisplay implements Observer {
private Subject weatherData;
private float temperature;
private float humidity;
private float pressure;
public ForecastDisplay(Subject weatherData) {
this.weatherData = weatherData;
// 註冊觀察者
weatherData.registerObserver(this);
}
@Override
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("Forecast:waiting for implementation...");
}
}
(5)測試
public class Test {
public static void main(String[] args) {
// 氣象資料
WeatherData weatherData = new WeatherData();
// 目前狀態佈告板
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
// 統計佈告板
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
// 預測佈告板
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
// 模擬氣象觀測值變化
weatherData.setMeasurements(80, 65, 30.4F);
weatherData.setMeasurements(82, 70, 29.2F);
weatherData.setMeasurements(78, 90, 29.2F);
}
}