观察者模式

有一个设计模式帮助你的对象知悉现状,不会错过该对象感兴趣的事情,甚至在对象运行时可决定是否要继续被通知,观察者模式是JDK中使用最多的设计模式之一,非常有用。无论是在JDK还是Android开发当中,我们很容易发现观察者模式的运用之处,如我们经常遇到的点击事件,通过Button控件的诸如Listener 的方法,onClickListener就是观察/订阅到了按钮的点击事件,从而就可以执行对相应的逻辑,不同的动作会有不同的观察者,如单击、长按、连续两次点击等都有对应的Listener。

观察者模式概述

主题 + 订阅者 = 观察者设计模式

现在假设有一个气象站,气象站会根据天气的变化设置新的气象数据 (温度、湿度、气压) ,这些数据会展示在气象看板上面,一旦气象站发布了新的数据,则看板也必须立马更新展示的数据。

mark

在这个例子中,主题就是天气数据,订阅者就是显示装置。一旦有新的天气数据,显示装置立马展示新的数据。

观察者模式代码实现

mark

首先定义一个主题的接口,所有的主题都需要实现这个接口:

 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}

接下来定义一个展示数据的接口,作为显示装置都需要实现的接口:

1public interface DisplayElement {
2    void display();
3}

然后就是显示装置的具体实现,目前只实现一种那就是展示最新的气象数据:

 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}

mark

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内置观察者重做气象站

mark

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,这违反了“多用组合、少用继承”的原则。平时使用的时候应该多注意这个问题,有必要的话最好自己实现一套观察者模式。

参考资料

《Head First 设计模式》