当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。
假如现在需要实现一个天气预报的服务,气象站可以用提供一些服务,例如温度、湿度和气压。因为这些数据的获取需要建立很多的观测站,投入很大,而各个应用想获取天气情况的时候没有必要自己建造,只需要从气象站提供的接口获取就可以。假设应用A 需要展示当前的天气情况,应用B需要展示天气情况的历史统计,应用C需要展示未来天气预报。可以用以下的图来表示这个情况:
如果按照气象站给的接口,我们可以进行一个简单的实现。
public class WeatherData{
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);
}
}
上边的代码实现,虽然能满足现在的要求,但是还是存在以下几个问题:
1.针对的是具体实现编程而不是针对接口。
2.无法在运行时动态地增加或删除应用。
3.变化的部分没有和不变的部分分开(请参照策略模式)。
那什么是观察者模式呢?举个例子,我们使用微信公众号的模式,就是一个典型的观察者模式。简而言之,微信公众号 + 用户 = 观察者模式。在观察者模式中,公众号被叫做“主题”(Subject),用户被称之为“观察者”(Obsever)。观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象状态改变时,它的所有依赖对象都会收到通知并自动跟新。观察者模式定义了一种对象设计,让主题和观察者之间松耦合。当两个对象之间松耦合,它们之间依然可以交互,但是不太清楚彼此之间的细节。下面是观察者模式的类图:
松耦合的设计之所以能够让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的依赖降到了最低。所以我们的设计原则之一就是为了交互对象之间的松耦合而努力。根据上边的类图以及描述,我们可以自己编写一个案例的实现,虽然Java内置了对观察者模式的支持,但是我们先自己实现一遍。
/**
* @author MiaoCheng
* @date: Create in 2018/7/13
* @description: 主题类接口
* @modifide By:
*/
public interface Subject {
/**
* 注册观察者方法
* @param observer 需要传入观察者对象
*/
void registerObserver(Observer observer);
/**
* 删除观察者方法
* @param observer 需要传入观察者对象
*/
void removeObserver(Observer observer);
/**
* 当状态改变时,此方法被调用
*/
void notifyObserver();
}
/**
* @author MiaoCheng
* @date: Create in 2018/7/13
* @description: 观察者接口
* @modifide By:
*/
public interface Observer {
/**
* 当状态改变时此方法会被调用
* @param temp 温度
* @param humidity 湿度
* @param pressure 气压
*/
void update(float temp, float humidity, float pressure);
}
/**
* @author MiaoCheng
* @date: Create in 2018/7/13
* @description: 当应用需要展示时,调用此对象
* @modifide By:
*/
public interface DisplayElement {
/**
* 展示信息
*/
void display();
}
接下来,我们开始实现WeatherDate类,对比第一次实现的类,看看有什么不一样。
/**
* @author MiaoCheng
* @date: Create in 2018/7/16
* @description: WeatherData类的实现
* @modifide By:
*/
public class WeatherData implements Subject {
/**
* 记录观察者列表
*/
private ArrayList observers;
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 压力
*/
private float pressure;
public WeatherData(){
observers = new ArrayList();
}
@Override
public void registerObserver(Observer observer) {
// 注册观察者,只需要把观察者对象添加到列表中
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
// 移除观察者,需要把观察者从列表中删除
int i = observers.indexOf(observer);
if(i >= 0 ){
observers.remove(i);
}
}
@Override
public void notifyObserver() {
// 循环通知每一个观察者更新数据,由于每个观察者都实现update方法,所以知道怎么更新
for(int i = 0; i < observers.size(); i++){
Observer observer = (Observer) observers.get(i);
observer.update(temperature, humidity, pressure);
}
}
public void measurementsChange(){
//当更新数据时,通知观察者
notifyObserver();
}
public void setMeasurements(float temperature, float humidity, float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChange();
}
}
/**
* @author MiaoCheng
* @date: Create in 2018/7/16
* @description: 显示当前天气的应用
* @modifide By:
*/
public class CurrentConditionDisplay implements Observer, DisplayElement{
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 压力
*/
private float pressure;
private Subject weatherData;
/**
* 通过构造器注册观察者对象
* @param weatherData
*/
public CurrentConditionDisplay(Subject weatherData){
this.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 + "Pa");
}
}
接下来就是测试之前实现的功能了,目前只实现了显示当前天气的应用:
/**
* @author MiaoCheng
* @date: Create in 2018/7/16
* @description: 测试显示当前天气的应用
* @modifide By:
*/
public class WeatherStationTest {
@Test
public void weatherTest(){
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
weatherData.setMeasurements(22f,40f,20f);
}
}
结果:
如果需要添加新的应用,只需要实现对应的接口,并且注册,就可以接收到观察者发布的消息。如果不想接受,只需要调用对应的remove接口就可以。现在已经简单实现了一个观察者模式类,但是Java API内置了对观察者模式的支持。接下来再通过Java内置支持实现一下。以下是类图:
在java.util包中包含了一个Observer接口和一个Observable类,上边的实现方式是使用推送的方式更新信息,如果使用Java内置支持,就可以使用主动拉取信息的方式来更新数据。Observer类对应的是观察者,Observable对应的是主题。
import java.util.Observable;
/**
* @author MiaoCheng
* @date: Create in 2018/7/16
* @description: 通过继承 Observable 对象实现主题方法
* @modifide By:
*/
public class WeatherData extends Observable {
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 压力
*/
private float pressure;
/**
* 因为在父类中实现了对观察者的注册、删除等方法,所以不再需要记录观察者列表
*/
public WeatherData(){
}
public void measurementsChange(){
// 在通知改变之前,要先调用setChange方法表示状态已经改变,此方法可以灵活调用,
// 例如温度每改变一度才通知观察者,而不是只要改变就通知
setChanged();
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChange();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
/**
* @author MiaoCheng
* @date: Create in 2018/7/16
* @description: 显示当前天气的应用
* @modifide By:
*/
import java.util.Observable;
import java.util.Observer;
public class CurrentConditionDisplay implements Observer, DisplayElement{
/**
* 观察者对象
*/
Observable observable;
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 压力
*/
private float pressure;
/**
* 通过构造器注册观察者对象
* @param observable
*/
public CurrentConditionDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this);
}
/**
* 更新数据,增加了一个参数作为附属信息
* @param obs
* @param arg
*/
@Override
public void update(Observable obs, Object arg) {
if(obs instanceof WeatherData){
WeatherData weatherData = (WeatherData) obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
this.pressure = weatherData.getPressure();
display();
}
}
@Override
public void display() {
System.out.println("当前温度:"+ temperature+"℃ 当前湿度:" + humidity +" % 当前气压:" + pressure + "Pa");
}
}
这个结果和之前的运行结果是一样的,使得我们对于观察者模式的实现更加方便了。