观察者模式
有一个设计模式帮助你的对象知悉现状,不会错过该对象感兴趣的事情,甚至在对象运行时可决定是否要继续被通知,观察者模式是JDK中使用最多的设计模式之一,非常有用。无论是在JDK还是Android开发当中,我们很容易发现观察者模式的运用之处,如我们经常遇到的点击事件,通过Button控件的诸如Listener 的方法,onClickListener就是观察/订阅到了按钮的点击事件,从而就可以执行对相应的逻辑,不同的动作会有不同的观察者,如单击、长按、连续两次点击等都有对应的Listener。
观察者模式概述
主题 + 订阅者 = 观察者设计模式
现在假设有一个气象站,气象站会根据天气的变化设置新的气象数据 (温度、湿度、气压) ,这些数据会展示在气象看板上面,一旦气象站发布了新的数据,则看板也必须立马更新展示的数据。
在这个例子中,主题就是天气数据,订阅者就是显示装置。一旦有新的天气数据,显示装置立马展示新的数据。
观察者模式代码实现
首先定义一个主题的接口,所有的主题都需要实现这个接口:
1/**
2 * 主题
3 */
4public interface Subject {
5 /**
6 * 注册观察者
7 * @param o 观察者对象
8 */
9 void registerObserver(Observer o);
10
11 /**
12 * 移除观察者
13 * @param o 观察者对象
14 */
15 void removeObserver(Observer o);
16
17 /**
18 * 通知观察者
19 */
20 void notifyObserver();
21}
天气数据就是一个主题,因此定义出天气主题的类:
1import java.util.ArrayList;
2import java.util.List;
3
4public class WeatherData implements Subject {
5 // 存储了此主题的观察者
6 private List<Observer> observers;
7
8 // 温度
9 private float temp;
10 // 湿度
11 private float humidity;
12 // 气压
13 private float pressure;
14
15 public WeatherData() {
16 this.observers = new ArrayList<>();
17 }
18
19 @Override
20 public void registerObserver(Observer o) {
21 observers.add(o);
22 }
23
24 @Override
25 public void removeObserver(Observer o) {
26 int index = observers.indexOf(o);
27 if(index >= 0) observers.remove(index);
28 }
29
30 @Override
31 public void notifyObserver() {
32 for(Observer o: observers){
33 o.update(temp, humidity, pressure);
34 }
35 }
36
37 /**
38 * 从气象站得到更新的观测值,通知观察者
39 */
40 public void measurementsChanged(){
41 notifyObserver();
42 }
43
44 /**
45 * 气象站设置新的值
46 */
47 public void setMeasurements(float temp, float humidity, float pressure){
48 this.temp = temp;
49 this.humidity = humidity;
50 this.pressure = pressure;
51 measurementsChanged();
52 }
53}
上面的观察者还未定义呢,还是先定义一个统一的观察者数据更新方法的接口
1/**
2 * 观察者
3 */
4public interface Observer {
5 /**
6 * 更新展示板
7 * @param temp 温度
8 * @param humidity 湿度
9 * @param pressure 气压
10 */
11 void update(float temp, float humidity, float pressure);
12}
接下来定义一个展示数据的接口,作为显示装置都需要实现的接口:
然后就是显示装置的具体实现,目前只实现一种那就是展示最新的气象数据:
1/**
2 * 布告板
3 */
4public class CurrentConditionDisplay implements Observer, DisplayElement{
5 private float temp;
6 private float humidity;
7
8 public CurrentConditionDisplay(Subject weatherData) {
9 weatherData.registerObserver(this);
10 }
11
12 public void stopDisplay(Subject weatherData){
13 weatherData.removeObserver(this);
14 }
15
16 @Override
17 public void display() {
18 System.out.println(temp + "℃ " + humidity + "%");
19 }
20
21 @Override
22 public void update(float temp, float humidity, float pressure) {
23 this.temp = temp;
24 this.humidity = humidity;
25 display();
26 }
27}
接下来测试一下写的观察者模式:
1public class Test {
2 public static void main(String[] args) {
3 WeatherData weatherData = new WeatherData();
4 // 把显示装置注册到WeatherData的观察者列表
5 CurrentConditionDisplay display = new CurrentConditionDisplay(weatherData);
6
7 weatherData.setMeasurements(80, 65, 30.4f);
8 weatherData.setMeasurements(85, 70, 32.2f);
9 weatherData.setMeasurements(86, 72, 36.3f);
10 weatherData.setMeasurements(89, 76, 38.0f);
11
12 System.out.println("==========================================");
13
14 // 停止观察
15 display.stopDisplay(weatherData);
16 weatherData.setMeasurements(91, 77, 39.5f);
17 weatherData.setMeasurements(97, 79, 40.2f);
18 }
19}
JDK内置的观察者模式
观察者模式是对象的行为模式,在对象之间定义了一对多的依赖关系,就是多个观察者和一个被观察者之间的关系,当被观察者发生变化的时候,会通知所有的观察者对象,他们做出相对应的操作。 在观察者模式,我们又分为推模型和拉模型两种方式,上面演示的内容是推模型。
在JDK内已经有实现好的观察者模式API,java.util包内包含最基本的Observer接口和Observable类,这与我们的Subject接口和Observer接口很相似。实际上Observer接口与Observable类使用起来更方便,因为很多功能已经提前准备好了。下面演示一个通过JDK的API实现拉模型的例子。
1、如何把对象变成观察者
实现观察者接口java.util.Observer,然后调用任何Observable对象的addObserver()方法,不想当观察者的时候,调用deleteObserver()方法即可。
2、被观察者如何送出通知
首先扩展java.util.Observer接口产生被观察者类,然后调用两个方法:
- 先调用setChanged()方法,标记状态已经改变的事实
- 然后调用notifyObservers()方法中的一个,notifyObservers()或者notifyObservers(Object arg)
3、观察者如何接收通知
同以前的update()方法一样,只是方法参数略有不同:
1update(Observable o, Object arg)
第一个参数Observable就是主题对象,好让观察者知道是哪个主题通知它的;第二个参数就是上面的例子中的参数,即数据对象。
如果使用推模式,则可以把数据当做数据对象传入notifyObservers(Object arg)中。否则观察者就必须从被观察者对象中拉取数据,我们把上面气象站的例子重做一次。
4、关于setChanged()
setChanged()方法用于标记状态已经改变的事实,好让notifyObservers()知道当它被调用时就应该更新观察者。如果调用notifyObservers()之前没有调用setChanged(),则观察者不会被通知,伪代码如下:
1setChaged(){
2 changed = true
3}
4
5notifyObservers(Object arg){
6 if(changed){
7 for obs in obsList {
8 call update(this, arg)
9 }
10 changed = false
11 }
12}
13
14notifyObservers(){
15 notifyObservers(null)
16}
这样做的目的就是在更新观察者的时候能有更多的弹性,比如在你想在气象温度变化0.5度以上才通知观察者,就需要通过调用setChanged这样的方式进行数据的有效更新。
JDK内置观察者重做气象站
WeatherData.java
1import java.util.Observable;
2
3public class WeatherData extends Observable {
4 // 温度
5 private float temp;
6 // 湿度
7 private float humidity;
8 // 气压
9 private float pressure;
10
11 public WeatherData() { }
12
13 public void measurementsChanged(){
14 setChanged();
15 notifyObservers();
16 }
17
18 /**
19 * 气象站设置新的值
20 */
21 public void setMeasurements(float temp, float humidity, float pressure){
22 this.temp = temp;
23 this.humidity = humidity;
24 this.pressure = pressure;
25 measurementsChanged();
26 }
27
28 // 观察者会利用这些Getter方法取得WeatherData的状态
29 public float getTemp() {
30 return temp;
31 }
32
33 public float getHumidity() {
34 return humidity;
35 }
36
37 public float getPressure() {
38 return pressure;
39 }
40}
CurrentConditionDisplay.java (DisplayElement和前面的例子一样)
1import java.util.Observable;
2import java.util.Observer;
3
4public class CurrentConditionDisplay implements Observer, DisplayElement {
5 private Observable observable;
6 private float temp;
7 private float humidity;
8
9 public CurrentConditionDisplay(Observable observable) {
10 this.observable = observable;
11 observable.addObserver(this);
12 }
13
14 @Override
15 public void display() {
16 System.out.println(temp + "℃ " + humidity + "%");
17 }
18
19 @Override
20 public void update(Observable o, Object arg) {
21 //System.out.println("被观察者:" + o.getClass().getName());
22 if(o instanceof WeatherData){
23 WeatherData weatherData = (WeatherData) o;
24 this.humidity = weatherData.getHumidity();
25 this.temp = weatherData.getTemp();
26 display();
27 }
28 }
29}
主题对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。一般这种模型的实现中,会把主题对象自身通过update()方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
观察者模式的优缺点
1、优点
首先是松耦合,当两个对象之间松耦合,它们依旧可以交互,但是不太清楚彼此的细节,观察者模式就提供了这样一种对象设计,让主题和观察者之间松耦合。
主题值需要知道观察者实现了某个接口,也就是Observer接口,不需要知道具体观察者实现类是什么,也不用关系观察者的实现细节。任何时间我们都可以动态的添加或者移除观察者、也包括替换新的观察者等操作,主题都不会受到影响。改变被观察者和观察者任意一方都不会影响另一方,这就是松耦合特点。
2、缺点
接下来说说缺点, 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间,也就是说同一个主题的观察者不能太多,太多了每次通知都是需要消耗时间的。
而且如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。
接下来讨论一个问题,但是却不是观察者模式的问题,而是JDK内置的观察者模式的问题。java.util.Observable是一个类而不是一个接口,如果要使用必须继承这个类,这其实限制了Observable的复用能力,而且通过源码可以看到setChanged()是受保护的权限,这意味着只能继承java.util.Observable,这违反了“多用组合、少用继承”的原则。平时使用的时候应该多注意这个问题,有必要的话最好自己实现一套观察者模式。