定义
观察者模式定义了对象之间的一种一对多的依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
设计原则
- 封装变化:找出程序中会变化的方面,然后将其和固定不变的方面相分离。
- 针对接口编程,不针对实现编程:实现松耦合,利于扩展,实现有弹性的oo系统。
- 多用组合,少用继承:继承会使实现类变得异常冗余。
气象站实现
1. 自定义观察者模式
被观察者成为主题subject,观察者称为observer,可以向subject中注册多个observer,当subject状态改变时,通知所有的observers。
(1)定义subject和observer的接口:
public interface Subject {
//向主题中注册观察者;
void registerObserver(Observer o);
//移除主题中的观察者;
void removeObserver(Observer o);
//通知观察者;
void notifyObservers(double temperature, double humidity, double pressure);
}
public interface Observer {
//更新观察者的数据;
void update(double temp, double humidity, double pressure);
//注销当前的观察者;
void quit();
}
public interface Board {
//展示发布平台内容;
void display();
}
(2)接口的实现类:
/**
* 气象站类,负责收集气象数据:气温,湿度,气压.
* 当数据改变时,通知各天气发布平台.
* Created by sarahzhou on 16/6/15.
*/
public class WeatherStation implements Subject {
private double temperature;
private double humidity;
private double pressure;
private ArrayList<Observer> observers;
//标记数据是否更新;
private boolean changed = false;
public WeatherStation() {
this.observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
if (!observers.contains(o)) {
observers.add(o);
}
}
@Override
public void removeObserver(Observer o) {
if (observers.contains(o)) {
observers.remove(o);
}
}
@Override
public void notifyObservers(double temperature, double humidity, double pressure) {
int count = observers.size();
for (int i = 0; i < count; i++) {
observers.get(i).update(temperature, humidity, pressure);
}
}
//更新天气数据;触发通知更新方法notifyObservers
public void updateData(double temp, double humidity, double pressure) {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
notifyObservers(temp, humidity, pressure);
}
}
/**
* 实时天气发布平台
* Created by sarahzhou on 16/6/15.
*/
public class CurrentWeatherBoard implements Observer, Board {
private double temperature;
private double humidity;
private double pressure;
private Subject subject;
//注册当前观察者;
public CurrentWeatherBoard(Subject subject) {
subject.registerObserver(this);
this.subject = subject;
}
@Override
public void update(double temp, double humidity, double pressure) {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
display();
}
@Override
public void quit() {
this.subject.removeObserver(this);
}
@Override
public void display() {
System.out.println("Current:气温=" + this.temperature + ",湿度=" +
this.humidity + ",气压=" + this.pressure);
}
}
/**
* 天气预报发布平台
* Created by sarahzhou on 16/6/15.
*/
public class ForecastBoard implements Observer, Board {
private double temperature;
private double humidity;
private double pressure;
private Subject subject;
//注册观察者;
public ForecastBoard(Subject subject) {
subject.registerObserver(this);
this.subject = subject;
}
@Override
public void update(double temp, double humidity, double pressure) {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
display();
}
@Override
public void quit() {
this.subject.removeObserver(this);
}
@Override
public void display() {
System.out.println("Forecast:气温=" + this.temperature + ",湿度=" +
this.humidity + ",气压=" + this.pressure + "预计未来3天将有雷雨天气!");
}
}
(3)测试结果:
@Test
public void test1() {
WeatherStation station = new WeatherStation();
Observer o1 = new CurrentWeatherBoard(station);
Observer o2 = new ForecastBoard(station);
station.updateData(31.5, 68.5, 99.0);
System.out.println("======================");
station.updateData(25.0, 77.5, 85.0);
System.out.println("\n==========预报平台Forecast退出============");
o2.quit();
station.updateData(15.5, 85.0, 90.5);
}
数据结果为:
我们可以看到,当subject的数据更新时,观察者即可收到通知并做出相应的输出。observer负责注册和注销,subject关注推送数据,没错,我们这里使用了”推送”push这个词,有时候observer并非需要全部的数据,但是呢,我们的subject不管三七二十一把所有的数据全都push给了observer,notifyObservers(double temperature, double humidity, double pressure);
我们可能已经发现了问题:首先,observer可能收到了太多不需要的数据,如果数据很多,那么我们的方法参数岂不是很长?其次,接口的耦合性太高了,气象站要增加一个‘风力’的参数,想一下我们要变动哪些地方?subject和observer的接口要变动,其对应的实现类全部都要变动,而我们的设计原则是松耦合,将变化的部分和固定的部分分离,so我们可以采用pull拉的方式来传递数据。
在notifyObservers的时候将整个subject传递给观察者,
void notifyObservers(Subject subject);
在subject的实现类中给出每个数据的getter方法供观察者调用,对于观察者,执行update的时候,调用subject的getter方法来拉取自己需要的数据,
void update(Subject subject);
这样,我要增加‘风力’这个数据,接口不需要变动,只用在WeatherStation中增加windForce字段和它的getter方法就好啦。
2. java.util提供的观察者模式API
java为我们提供的观察者模式API包含java.util.Observable和java.util.Observer,前者是subject,后者是观察者。我们来分析下它们的code。
(1)java.util.Observer接口:
public interface Observer {
/**
* This method is called whenever the observed object is changed. An
* application calls an <tt>Observable</tt> object's
* <code>notifyObservers</code> method to have all the object's
* observers notified of the change.
*
* @param o the observable object.
* @param arg an argument passed to the <code>notifyObservers</code>
* method.
*/
void update(Observable o, Object arg);
这个接口很简单,我们自己的观察者只用实现它,在它基础上扩展就OK了,这里java即支持push推的方式,也支持pull拉的方式。若要push数据,subject需要这样调用观察者的update方法:update(null,arg);
,若要pull数据,subject需要这样调用观察者的update方法:update(subject,null);
(2)java.util.Observable类:
public class Observable {
/** 用来标记该主题是否发生了变化; */
private boolean changed = false;
private Vector<Observer> obs;
/** 创建主题实例的时候,新建一个空的容器用来存储观察者; */
public Observable() {
obs = new Vector<>();
}
/** 添加新的观察者 */
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
/** 删除指定的观察者; */
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
/** 通知观察者,不传递参数; */
public void notifyObservers() {
notifyObservers(null);
}
/** 通知方法的具体逻辑, */
public void notifyObservers(Object arg) {
Object[] arrLocal;
//判断主题是否变化,若已变化,则将所有观察者obs存入临时变量arrLocal待处理,并清除changed标记。这里需要用synchronized进行线程同步,防止多线程同时作用时发生数据错误。
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
//遍历所有的观察者,调用其update方法;
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
//删除注册的所有的观察者;
public synchronized void deleteObservers() {
obs.removeAllElements();
}
//若发生变化,置changed为true,需要在我们的实现类中去调用;
protected synchronized void setChanged() {
changed = true;
}
//清除changed标记;
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
public synchronized int countObservers() {
return obs.size();
}
}
(3)使用java提供的API实现气象站:
public class WeatherStation extends Observable {
private double temperature;
private double humidity;
private double pressure;
public void setData(double temp, double humidity, double pressure) {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
setChanged();
notifyObservers();
}
public double getTemperature() {
return this.temperature;
}
public double getHumidity() {
return this.humidity;
}
public double getPressure() {
return this.pressure;
}
}
这里我们采用pull拉取数据的方式;
public class ForecastBoard implements Observer, Board {
private double temperature;
private double humidity;
@Override
public void update(Observable o, Object arg) {
if (o instanceof WeatherStation) {
WeatherStation station = (WeatherStation) o;
this.temperature = station.getTemperature();
this.humidity = station.getHumidity();
display();
}
}
@Override
public void display() {
System.out.println(this.toString() + ":气温=" +
this.temperature + ",湿度=" + this.humidity);
}
}
是不是很简单呢。
测试结果如下:
@Test
public void test2() {
WeatherStation station = new WeatherStation();
ForecastBoard board1 = new ForecastBoard();
ForecastBoard board2 = new ForecastBoard();
station.addObserver(board1);
station.addObserver(board2);
station.setData(34.5, 89.0, 98.5);
System.out.println("\n============移除board2============");
station.deleteObserver(board2);
station.setData(25.5, 75.0, 88.5);
}
jdk.ForecastBoard@23ab930d:气温=34.5,湿度=89.0
jdk.ForecastBoard@4534b60d:气温=34.5,湿度=89.0
============移除board2============
jdk.ForecastBoard@4534b60d:气温=25.5,湿度=75.0
总结
其实观察者模式在我们的开发中很常用。前端开发中的事件绑定事件触发,比如为一个button绑定一个click事件,我们通常使用οnclick=’js方法();’,或者使用jquery来绑定,实际上,是调用了button组件的addActionListener方法,为其注册了监听click事件的ActionListener(即为观察者),点击了button之后,触发ActionListener的actionPerformed方法,并封装了ActionEvent对象来调用public void actionPerformed(ActionEvent e);
在这里,button是主题,ActionListener是观察者,ActionEvent是通知观察者时传递的数据。
java web模型也是一个典型的观察者模型的应用。服务器启动后,ServletContextListener一直监听浏览器请求,接收到请求后,解析分发,并作后台处理。总之,观察者模式很经典,使用中要会举一反三。