观察者模式(observer)
文章说明
- 该文章为
《Head First 设计模式》
的学习笔记 - 非常推荐大家去买一本
《Head First 设计模式》
去看看,节奏轻松愉悦,讲得通俗易懂,非常适合想要学习、了解、应用设计模式以及OO设计原则的小白。
提出问题
- 气象站会给我们一个WeatherData的类,类图如下(当然类图中还有其他的方法没有显现出来,这里只展现对我们来说最重要的部分):
- 很显然我们可以从这
getTemperature() getHumidity() getPressure()
三个方法中获取温度、湿度、压力的值 - 气象站的程序员告诉我们这个
measurementsChanged()
方法会在气象台数据测量跟新后被自动调用。 - 我们的工作是实现
measurementsChanged()
,好让它更新目前状况、气象统计、天气预报的告示板。 - 怎么才能设计一个牵一发而动全身的系统?当要
WeatherData
的状态更新,所有的布告板都同步数据,并且系统的可扩展性、可维护性要好,可以随时添加新的或则删除旧的布告板。
认识观察者模式
举一个十分容易理解的例子:
报纸和杂志订阅
- 报社的业务就是初版报纸。
- 向某家报社订阅报纸,只要他们有新报纸初版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸。
- 当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
- 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或者取消订阅报纸。
- 怎么样?是不是脑子里有了些观察者模式的概念了?
- 报社 --> 气象站,报纸 -->
WeatherData
对象 - 订阅报纸 --> 注册成为气象站的服务对象,给订户发新报纸的行为 --> 气象站更新了测量数据并发给了所有布告板
- 订阅报纸或者取消订阅报纸 --> 添加或删除布告板
- 初版者 + 订阅者 = 观察者模式
- 气象站 + 布告板 = 观察者模式
- 主题(可观察者) + 观察者 = 观察者模式
模式的定义
定义了对象之间一对多依赖,这样一来,当一个对象改变状态时,所有的依赖者都会收到通知并自动更新。
涉及的OO设计原则
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不针对实现编程
- 为交互对象之间的松耦合设计而努力
自定义观察者模式
Java对观察者模式有内置的支持,这个我们稍后讲到,自己动手构建观察者模式并不难,而且更有弹性,因为内置的Observable
(作为可观察者要扩展的类)并不是接口而是普通的类。
类图
上面的类图并没有指明主题和观察者们怎么建立关系,也就是一对多的关系依赖,这里用到了松耦合的设计。
松耦合的设计
类图
一对多关系
可以看到多个观察者对应一个主题,而同一个主题拥有多个观察者。
松耦合的实现
以气象台为例子
WeatherData
通过集合拥有(has-a)多个布告板
List<Observer> observers;
- 而布告板如
CurrentConditionsDisplay
通过有参构造函数获得weatherData
的引用但不存储一份对象,仅仅使用它的方法注册或者注销成为观察者。
public CurrentConditionsDisplay(Subject weatherData) {
weatherData.registerObserver(this); // 注册成为观察者
}
- 当两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此之间的细节。
- 观察者模式提供了一种松耦合的设计,让主题和观察者之间送耦合。
很显然可以看出,WeatherData
不需要知道你是谁?只需要知道你实现了Observer
,而对于布告板来说,也不要知道你是谁?只需要知道你实现了Subject
。
部分代码的展示
主题:WeatherData.java
/**
* 气象台提供的WeatherData类
* 包含了部分气象数据
* 作为观察者模式的主题
*/
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature, humidity, pressure;
public WeatherData() {
observers = new ArrayList<Observer>();
}
// 注册成为观察者
public void registerObserver(Observer o) {
// 不可以重复注册,这里可以使用Set<Observer>
if(observers.contains(o))
return ;
observers.add(o);
System.out.println("恭喜 " + o + " 注册策成为我的观察者之一");
System.out.println("观察者们:" + observers);
}
// 注销成为观察者
public void removeObserver(Observer o) {
if(observers.contains(o))
observers.remove(o);
System.out.println("很高兴为您服务!" + o + "随时欢迎您再加入观察者联盟~");
System.out.println(observers);
}
// 当状态改变时,告知所有观察者
public void notifyObserver() {
for(Observer o : observers)
o.update(temperature, humidity, pressure);
}
// 当新的测量数据备妥时,会自动被调用(我们不在乎方法如何被调用,我们只在乎它被调用了)
public void measurementsChanged() {
notifyObserver();
}
// 设置气象数据
public void setMeasurements(
float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObserver();
}
// WeatherData的其他方法...
}
Interfaces: Subject,Observer,DisplayElement
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObserver();
}
public interface Observer {
void update(float temp, float humi, float pres);
}
public interface DisplayElements {
void display();
}
观察者:CurrentCoditionsDisplay.java
这里仅仅展示其中一个布告板
/**
* 创建目前状况的布告板
* 即展示当前的温度,湿度,气压
*/
public class CurrentConditionDisplay implements Observer, DisplayElements {
private float temperature, humidity, pressure;
public CurrentConditionDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
public void display() {
System.out.println(
"目前状况:温度[" + temperature +
"F] 湿度[" + humidity +
"%] 气压[" + pressure + "Pa]");
}
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}
测试类:WeatherStation.java
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay ccd =
new CurrentConditionDisplay(weatherData);
StatisticsDisplay sd =
new StatisticsDisplay(weatherData);
ForecastDisplay fd =
new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
运行结果:
恭喜 CurrentConditionDisplay 注册策成为我的观察者之一
观察者们:[CurrentConditionDisplay]
恭喜 StatisticsDisplay 注册策成为我的观察者之一
观察者们:[CurrentConditionDisplay, StatisticsDisplay]
恭喜 ForecastDisplay 注册策成为我的观察者之一
观察者们:[CurrentConditionDisplay, StatisticsDisplay, ForecastDisplay]
目前状况:温度[80.0F] 湿度[65.0%] 气压[30.4Pa]
统计:温度[平均值/最大值/最小值] : 80.0/80.0/80.0
预测:天气在好转
目前状况:温度[82.0F] 湿度[70.0%] 气压[29.2Pa]
统计:温度[平均值/最大值/最小值] : 81.0/82.0/80.0
预测:小心阴雨天气
目前状况:温度[78.0F] 湿度[90.0%] 气压[29.2Pa]
统计:温度[平均值/最大值/最小值] : 80.0/82.0/78.0
预测:天气没有什么变化
Java内置的观察者模式
如何运作?
观察者
- 首先观察者实现
java.util.Observer
接口。 - 然后调用任何
Observable
对象的addObserver()
方法。 - 当不再想当观察者时,调用
deleteObserver()
方法。
可观察者
- 扩展即继承
java.util.Observable
类,注意这是一个类。
可观察者怎么送出通知…
- 先调用
setChange()
方法,标记状态已经改变的事实。 - 然后调用两种
notifyObservers()
方法中的一种: notifyObservres()
或者notifyObservers(Object arg)
- 一种是"拉",另一种是"推"两种方式都可以,但是"推"配合setter方法一起用效果更佳。
观察者如何收到通知…
- 观察者实现了更新方法,方法的特征签名稍有不同:
update(Observable o, Object arg)
- 当可观察者用"推"的方式发送消息时,Object arg 则为
null
关于setChange()方法
setChange()
方法不是可有可无的,它的存在可以更好的控制发送消息的行为,有更多的弹性。clearChange()
方法,将changed
状态设置回false
。hasChanged()
方法,告诉你changed
标志的当前状态。
部分代码展示
Java内置支持的方式重做气象站的例子
可观察者:WeatherData.java
import java.util.Observable;
/**
* 用java内置的支持实现观察者模式
* 让WeatherData扩展Observable成为可观察者,也就是主题
*/
public class WeatherData extends Observable {
private float temperature, humidity, pressure;
public WeatherData() {}
public void measurementsChanged() {
setChanged(); // 改变状态标志
notifyObservers();
}
public void setMeasurements(
float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// 要使用“拉”的做法,定义setter方法
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
观察者:CurrentConditionDisplay.java
这里仅仅展示其中一个布告板
import java.util.Observable;
import java.util.Observer;
/**
* 用"拉"的做法重写目前状况的布告板
*/
public class CurrentConditionDisplay implements Observer, DisplayElements {
private float temperature, humidity, pressure;
public CurrentConditionDisplay(Observable observable) {
// 注册成为观察者
observable.addObserver(this);
}
public void display() {
System.out.println(
"目前状况:温度[" + temperature +
"F] 湿度[" + humidity +
"%] 气压[" + pressure + "Pa]");
}
public void update(Observable o, Object arg) {
// 这里o是指扩展了Observable的WeatherData类
if(o instanceof WeatherData) { // "拉"的实现
WeatherData weatherData = (WeatherData)o;
temperature = weatherData.getTemperature();
humidity = weatherData.getHumidity();
pressure = weatherData.getPressure();
display();
}
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}
运行结果:
统计:温度[平均值/最大值/最小值] : 80.0/80.0/200.0
预测:天气在好转
目前状况:温度[80.0F] 湿度[65.0%] 气压[30.4Pa]
统计:温度[平均值/最大值/最小值] : 81.0/82.0/200.0
预测:小心阴雨天气
目前状况:温度[82.0F] 湿度[70.0%] 气压[29.2Pa]
统计:温度[平均值/最大值/最小值] : 80.0/82.0/78.0
预测:天气没有什么变化
目前状况:温度[78.0F] 湿度[90.0%] 气压[29.2Pa]
- 可以看到通知观察者的次序不同我们先前的次序
- 所以不能依赖于观察者被通知的次序
黑暗面
java.util.Observable
是一个类,而且还不是抽象类。- 这意味着我们不能创建自己的可观察者。
- 并且当我们想同时拥有Observable类和另一个超类的行为,比如我是一个可观察者并且我也是学生,但是这做不到,因为Java不支持多继承。
Observable
还将setCahgned()
保护起来(定义为protected
),这意味着:除非你继承自Observable,否则你无法创建Observable实例并组合到你自己的类中。这违反了"多用组合,少用继承"的设计原则。
观察者模式是JDK中使用最多的模式
例如:
JavaBeans
Swing
RMI