设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》,作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates。
在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。
在观察者模式当中,分为观察者和信息发布者,当信息发布者想要发布一些消息时,那么这些观察者就都能接收到这些消息。
在现实生活中就有观察者模式的使用案例,例如社会上的报社,报社的任务是出版报纸,人们如果对这家报社出版的报纸感兴趣,那么就会订阅这家报社的消息,当这家报社出新的报纸时,这家报社的众多订阅者都会接收到新的报纸,当然如果对报社的消息不再感兴趣。那么随时可以取消订阅,那么这个人便再也不会收到报社的消息了。
定义了对象和对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
出版者(主题Subject) + 订阅者(观察者Observer) = 观察者模式
体现了面向接口(超类型)编程的设计方式:
Observer(观察者)
update():
观察者通过本类中的这个方法,从外界获得自己感兴趣的消息。
Subject(消息发布者)
registerObserver():
注册观察者,即将观察者的引用放到本类的集合中,当需要向外发布消息的时候,会遍历本类中的集合,挨个拿出观察者对象,并调用这些对象的update()将消息放入各个观察者的类中。
removeObserver():
移除观察者,当观察者对消息不再感兴趣时,消息发布者会将这个观察者的对象的引用,从观察者集合中移除。
notifyObserver():
遍历观察者集合,依次调用对象的update()进行消息通知。
在观察者模式中,我们可以感受到松耦合的好处,即消息发布者并不关心观察者的类的细节,只要求消息发布者是Observer就好了,这样无论有老的观察者取消消息的订阅,或者有新的观察者订阅了消息,都不会对主题Subject产生影响。同理Subject的变化也不会影响订阅者,因为Observer和Subject是松耦合的。
为了交互对象之间的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
现在有个气象站有如下需求,需要我们帮他们把检测到的原始数据显示到布告板上,并且他们会提供一个对象(WeatherData),这个对象可以获取到气象站的原始数据,我们再将这些原始数据显示到布告板上。气象站目前要求了三种布告板:目前状况(温度、湿度、气压),气象统计、天气预报,另外气象站还要求我们的API设计必须具有可扩展性,方便日后其它布告板的添加,总体需求展现为下图:
气象站说我们可以通过WeatherData拿到原始数据,那么我们可以看看这个对象中有哪些方法。
//气象站提供的获取原始数据的类
public class WeatherData {
//获得温度数据
public float getTemperature() {
System.out.println("模拟获取温度数据的过程...");
}
//获得湿度数据
public float getHumidity() {
System.out.println("模拟获取湿度数据的过程...");
}
//获得气压数据
public float getPressure() {
System.out.println("模拟获取气压数据的过程...");
}
/*
* 一旦气象测量更新,此方法会被调用
* 相当于报社有新的报纸,所以通知观察者的方法应该写在这个方法里
* 而上面三个方法,我们并不需要关心细节,直接拿来用就可以了
* */
public void measurementsChanged() {
// TODO 需要我们书写的部分
}
}
那么看一个错误的measurementsChanged()实现:
注:
currentConditionsDisplay(目前状况)、statisticsDisplay(气象统计)、forecastDisplay(天气预报)分别是三个布告板(即:三个观察者)。
public void measurementsChanged() {
float temp = getTemperature(); //获取温度
float humidity = getHumidity(); //获取湿度
float pressure = getPressure(); //获取气压
//向三个观察者发布消息,分别调用三个布告板对象,更新消息
currentConditionsDisplay.update(temp,humidity,pressure);
statisticsDisplay.update(temp,humidity,pressure);
forecastDisplay.update(temp,humidity,pressure);
}
上面的代码的问题是:
气象公司已经强调过,布告板以后可能会增加新的,或者有可能去掉某个旧的布告板,那么这样修改还是需要更改源代码,三个布告板对象全部都硬编码到程序中,这仍然是针对实现编程,我们需要改为针对接口编程。
上图中,三个布告板不仅实现了Observer接口,成为了观察者。还实现了DisplayElement接口,用于展示布告板的数据。
消息发布者接口:
//消息发布者的接口
public interface Subject {
//注册观察者
public void registerObserver(Observer o);
//移除观察者
public void removeObserver(Observer o);
//当主题状态改变时,此方法会被调用,用来通知观察者
public void notifyObservers();
}
观察者接口:
//观察者接口
public interface Observer {
//Subject通过本类中的Observer引用,调用update方法,将消息传递到Observer类中
public void update(float temp, float humidity, float pressure);
}
展示接口:
//展示接口
public interface DisplayElement {
//展示方法
public void display();
}
消息发布者的实现类WeatherData:
//可以提供基础气象数据的类
public class WeatherData implements Subject{
//Subject端保留的观察者集合,推送消息时,需要遍历这个集合
private ArrayList observers;
//温度
private float temperature;
//湿度
private float humidity;
//气压
private float pressure;
public WeatherData() {
observers = new ArrayList();
}
@Override
public void registerObserver(Observer o) {
// 观察者集合注册观察者
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
// 观察者集合中移除观察者
observers.remove(o);
}
@Override
public void notifyObservers() {
//遍历观察者集合
for (Observer observer : observers) {
//用观察者对象的引用,调用他自身的update方法,将数据传入观察者所在的类
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();
}
}
观察者的实现类-目前状况布告板:
//观察者的实现类-目前温度
public class CurrentConditionsDisplay implements Observer, DisplayElement{
//温度
private float temperature;
//湿度
private float humidity;
//气压
private float pressure;
//Subject的引用,保留此引用的作用:可以通过这个引用干其他的操作,例如取消注册
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
//虽然目前只利用weatherData进行了注册操作
weatherData.registerObserver(this);
}
//更新数据
@Override
public void update(float temp, float humidity, float pressure) {
//接收数据
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
//展示数据
display();
}
//展示数据
@Override
public void display() {
System.out.println("当前温度: " + temperature
+ "当前湿度:" + humidity
+ "气压:" + pressure);
}
}
观察者的实现类-气象统计:
//观察者实现类-气象统计
public class StatisticsDisplay implements Observer, DisplayElement{
//最高温度
private float maxTemp = 0.0f;
//最低温度
private float minTemp = 200;
//温度和
private float tempSum= 0.0f;
//气象更新次数
private int numReadings;
//消息发布者
private WeatherData weatherData;
public StatisticsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update(float temp, float humidity, float pressure) {
tempSum += temp;
numReadings++;
//得到最高温度
if (temp > maxTemp) {
maxTemp = temp;
}
//得到最低温度
if (temp < minTemp) {
minTemp = temp;
}
display();
}
@Override
public void display() {
System.out.println("平均/最高/最低 温度 = " + (tempSum / numReadings)
+ "/" + maxTemp + "/" + minTemp);
}
}
注:天气预告布告板这里省略。
测试类:
//气象显示测试类(天气预报同理,所以在这里省略)
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
//目前温度布告板订阅消息(注:ccd刚出生就注册了WeatherData)
CurrentConditionsDisplay ccd = new CurrentConditionsDisplay(weatherData);
//气象统计布告板订阅消息(注:sd刚出生就注册了WeatherData)
StatisticsDisplay sd = new StatisticsDisplay(weatherData);
//模拟气象变化:第一次变化
weatherData.setMeasurements(71, 72, 73);
System.out.println("---------------------------------------------------");
//模拟气象变化:第二次变化
weatherData.setMeasurements(81, 82, 83);
}
}
运行结果:
当前温度: 71.0,当前湿度:72.0,气压:73.0
平均/最高/最低 温度 = 71.0/71.0/71.0
---------------------------------------------------
当前温度: 81.0,当前湿度:82.0,气压:83.0
平均/最高/最低 温度 = 76.0/81.0/71.0
这样即使有新的布告板,那么我们只需要让这个新的布告板订阅这个消息即可。
但是,目前我们设计的观察者模式中,观察者完全处于被动状态,即只有消息发布者准备好了之后,才会将消息推送出去,在这之前,观察者对消息的进度毫不知情,所以这种观察者模式是一种“推送消息”的模式,那么有没有一种观察者主动取“拉取消息”的模式呢?
可参见下篇文章"《HeadFirst设计模式》第二章-观察者模式2"。