注:本篇博客内容出自Head First设计模式,仅对文章进行整理与总结。
1. 引出问题
假设我们现在需要做一个实时天气预报的功能,并可以自定义任意多个显示界面。我们可以通过一个WeatherData的java类获取想要的信息,并将获取的相应信息分别显示在不同的界面。WeatherData的功能如下:
类名 | 获取温度方法 | 获取湿度方法 | 获取气压方法 | 通知更新方法 |
WeatherData | getTempture | getHumidity | getPressure | measurementsChanged |
实现方案一:
public class WeatherData {
//略去实例变量声明
public void measurementsChanged() {
float temp = getTempture();
float humidity = getHumidity();
float pressure = getPressure();
//当前天气显示界面
currentConditionsDisplay.update(temp,humidity,pressure);
//数据分析建议界面
statisticsDisplay.update(temp,humidity,pressure);
//预报界面
forecastDisplay.update(temp,humidity,pressure);
}
//略去WeatherData的其他方法
}
根据之前提到的条件,天气发生变化时,measurementsChanged方法会被主动调用,我们直接在measurementsChanged方法里更新各个显示界面。
以上实现方法会遇到几个问题:
(1)当我们需要自定义新的显示界面时,就必须要修改WeatherDate类。
(2)根据策略模式中我们学到的知识,以上代码针对的是具体实现编程而不是接口,回想一下从策略模式学到的原则和概念。
(3)我们无法在运行时动态地增加或是删除指定的显示界面。
(4)我们破坏了WeatherData类的封装。
所以,实际开发中应该避免以上代码的使用。观察者模式正好解决了这部分问题。
2. 观察者模式
2.1 定义
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者收到通知并自动更新。
观察者模式中,被观察者,也可以说是被监听者称为主题,监听者称为观察者。如上情景中,WeatherData就是主题,而各种显示界面就是观察者。主题和观察者定义了一对多的。观察者依赖于此主题,只要主题状态一有变化,观察者就会被通知并更新自己的状态。
2.2 观察者模式类图
**UML图片使用的是ProcessOn绘制
2.3松耦合
当两个对象之间松耦合,他们仍然可以交互,但是不太清楚彼此的细节。观察者模式提供一种对象设计,让主题和观察者之间松耦合。
关于观察者的一切,主题只知道观察者实现了某个接口。主题不需要知道观察者的具体类是谁,有什么功能或者任何其他细节。
无论在什么时候观察者的增加,移除都不会对主题对象造成影响。这样我们就可以独立的复用主题或者观察者。如果我们在其他地方需要使用主题或者观察者,可以轻易的复用,因为二者并非时紧耦合的。
由此我们介绍一个新的设计原则:
为了交互对象之间的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
3 解决问题
好了,我们开始设计天气预报以及显示的功能,在此之前,先看一下下面的设计图吧。
大家可以对照上面的设计图自己试着先写写代码。
首先,创建出两个接口,分别表示主题和观察者,主题接口如下:
public interface Subject {
public void registerObserver(Observer o);
public void unregisterObserver(Observer o);
public void notifyObservers();
}
观察者接口如下:
public interface Observer {
public void update(float tempture,float humidity,float pressure);
}
显示接口:
public interface DisplayElement {
public void display();
}
以上3个接口分别对应设计图上面三个interface。
然后我们需要实现主题接口,由于我们需要从WeatherData获取数据并显示在显示屏上,所以WeatherData是实现了主题接口的具体类:
package org.canvas.doit;
import java.util.ArrayList;
import java.util.List;
public class WeatherData implements Subject {
private List<Observer> mObservers;
private float tempture;
private float humidity;
private float pressure;
public WeatherData() {
// TODO Auto-generated constructor stub
mObservers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
// TODO Auto-generated method stub
if (o != null) {
mObservers.add(o);
}
}
@Override
public void unregisterObserver(Observer o) {
// TODO Auto-generated method stub
if (o != null) {
int index = mObservers.indexOf(o);
if (index >=0) {
mObservers.remove(o);
}
}
}
@Override
public void notifyObservers() {
// TODO Auto-generated method stub
for(Observer observer : mObservers) {
observer.update(tempture, humidity, pressure);
}
}
public void measurementsChanged() {
notifyObservers();
}
//此方法方便我们在测试过程中改变信息数值
public void setMeasurements(float tempture,float humidity,float pressure) {
this.tempture = tempture;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTempture() {
return tempture;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
WeatherData类实现了Subject接口,并使用List存储了所有观察者。setMeasurements函数只是方便我们后期测试,这里不是具体实现去网络上拉数据,有兴趣可以自己试着从网上拉取气象数据之后显示,有很多免费的api接口可以调用。
现在又了具体的主题类,下面我们就来设计一些具体的观察者类。
从设计上看,我们有三个观察这类,当前观测值的观察者类:
package org.canvas.doit;
public class CurrentConditionsDisplay implements Observer,DisplayElement {
private float tempture;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject subject) {
// TODO Auto-generated constructor stub
this.weatherData = subject;
weatherData.registerObserver(this);
}
@Override
public void display() {
// TODO Auto-generated method stub
System.out.println("Current conditions : "+tempture+" F degrees and "+humidity+"% humitidy");
}
@Override
public void update(float tempture, float humidity, float pressure) {
// TODO Auto-generated method stub
this.tempture=tempture;
this.humidity = humidity;
display();
}
}
跟踪显示最小,平均,最大的观测值的统计观察者类:
package org.canvas.doit;
public class StatisticsDisplay implements Observer, DisplayElement {
private float pressure;
private float tempture;
private float humidity;
private Subject weatherData;
public StatisticsDisplay(Subject subject) {
// TODO Auto-generated constructor stub
this.weatherData = subject;
weatherData.registerObserver(this);
}
@Override
public void display() {
// TODO Auto-generated method stub
System.out.println("Statistics conditions : "+tempture+" F degree ,"+humidity+"% humidity and "+pressure+"Pa");
}
@Override
public void update(float tempture, float humidity, float pressure) {
// TODO Auto-generated method stub
this.pressure = pressure;
this.tempture = tempture;
this.humidity = humidity;
display();
}
}
最后是一个短期预报的观察者类:
package org.canvas.doit;
public class ForecastDisplay implements Observer,DisplayElement{
private Subject weatherData;
private float pressure;
private float tempture;
private float humidity;
public ForecastDisplay(Subject subject) {
// TODO Auto-generated constructor stub
this.weatherData = subject;
weatherData.registerObserver(this);
}
@Override
public void display() {
// TODO Auto-generated method stub
System.out.println("Forecast conditions : "+tempture+" F degree ,"+humidity+"% humidity and "+pressure+"Pa");
}
@Override
public void update(float tempture, float humidity, float pressure) {
// TODO Auto-generated method stub
this.pressure = pressure;
this.tempture = tempture;
this.humidity = humidity;
display();
}
}
还有一些自定义的显示信息,可以自行设计实现。
现在写了一Demo测试一下代码:
public static void main(String[] args) {
// TODO Auto-generated method stub
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay conditionsDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
System.out.println("\n================================================");
weatherData.setMeasurements(82,70, 29.2f);
System.out.println("\n================================================");
weatherData.setMeasurements(78, 90, 29.2f);
}
执行结果如下:
Current conditions : 80.0 F degrees and 65.0% humitidy
Statistics conditions : 80.0 F degree ,65.0% humidity and 30.4Pa
Forecast conditions : 80.0 F degree ,65.0% humidity and 30.4Pa
================================================
Current conditions : 82.0 F degrees and 70.0% humitidy
Statistics conditions : 82.0 F degree ,70.0% humidity and 29.2Pa
Forecast conditions : 82.0 F degree ,70.0% humidity and 29.2Pa
================================================
Current conditions : 78.0 F degrees and 90.0% humitidy
Statistics conditions : 78.0 F degree ,90.0% humidity and 29.2Pa
Forecast conditions : 78.0 F degree ,90.0% humidity and 29.2Pa
以上就是观察者模式的基本代码结果,对照设计图看应该能看明白。测试代码中,我们首先创建一个具体的主题和观察者,然后通过registerObserver将观察者注册到主题中去,这样当主题的数据有变化时,观察者就能收到相应的通知并更新显示信息。不仅如此,当我们创建新的观察者时,不需要修改主题代码进行适配,应因为我们的主题和观察者并不时紧耦合的而是松耦合设计。
4 Java内置的观察者模式
其实java有内置的观察者模式API。在java.util包中包含最基本的Observer接口和Observable类,这和我们设计的Subject和Observer很相似且基本原理也是一样的,有兴趣的话大家可以修改上面的代码使用java内置的观察者模式实现看看。
但我们很少使用内置的java设计者模式,那是因为Observable是一个类,所示使用的时候必须extends而不是implements。大家都知道java的单继承特性,如果我有一个类不仅要实现主题接口,还需要另一个超类的行为,这个时候就很麻烦处理,所以某些情况下不推荐使用java内置的观察者模式。
5 代码优化
上面虽然指出了java内置观察者接口的缺点,但也不是没有任何优点的。下面我们就学习java内置的观察者接口,优化我们自己的观察者模式。
5.1 更精确的控制
在Observable类中有一个函数:setChanged。此方法用来标记状态已经改变的事实。好让notifyObservers知道当他被调用时应该更新观察者。如果调用notifyObservers没有先调用setChanged,观察者就不会被通知。看一下Observable内部的具体实现如下:
setChanged(){
changed=true;
}
notifyObservers(Object org){
if(changed){
for every observer on the list{
call update(this,org);
}
changed=false;
}
}
notifyObservers(){
notifyObservers(null);
}
这样做可以让我们更新观察者时候有更多的弹性,比如说,上面的代码在获取温度显示的时候,假如温度只升高了0.1度,那我也会被通知更新显示,一般情况下时完全没有必要的,我想温度没升高0.5度的时候再去更新显示。此时,setChanged的功能就会派上用场了。虽然此功能可能用不到,但是预先准备的话就能在需要的时候随便使用了。
具体的代码实现就留给各位自己修改吧,应该不难吧。
5.2 更改数据获取的方式
如果你仔细观察上面的实现代码,就会发现一个严重的问题,那就是在观察者Observe接口中update函数的参数,到底这样写合不合适呢?
我们先来想一下,这样写确实能将主题的变化及时告诉观察者,但是这样一股脑的将数据传递过去,是不是不妥当,如果我只是需要一部分数据而不是全部数据,我就没必要去接受所有的数据。再者如果过段时间我需要额外在增加一个数据,比如说,温湿度指数,钓鱼指数或者洗车指数,那我就必须修改Observe接口中update函数的参数数量,这样的话,所有实现Observe的之类都需要一个一个的修改,得不偿失。
看到这里我想大家已经知道问题的严重性了,那应该怎么解决呢。这个问题其实不难,只需要我们在接到更新通知时自己去主题获取我们想要的数据就行了。比如说,当StatisticDisplay接受到更新通知时,我们可以通过WeatherData的getXXX函数自己获取自己想要的数据。这样update函数中的参数就不需要了,我们应该将参数删除。
以上也是java内置观察者类推荐的做法,不过内置类使用的是pull的方法。
以下是修改过后的Observer接口和CurrentConditionsDisplay观察者类:
public interface Observer {
public void update();
}
public class CurrentConditionsDisplay implements Observer,DisplayElement {
private float tempture;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject subject) {
// TODO Auto-generated constructor stub
this.weatherData = subject;
weatherData.registerObserver(this);
}
@Override
public void display() {
// TODO Auto-generated method stub
System.out.println("Current conditions : "+tempture+" F degrees and "+humidity+"% humitidy");
}
@Override
public void update() {
// TODO Auto-generated method stub
if (weatherData instanceof WeatherData) {
WeatherData data = (WeatherData)weatherData;
this.tempture=data.getTempture();
this.humidity = data.getHumidity();
display();
}
}
}
大概就是这个样子,其他的文件修改也很类似就不贴出来了。