观察者模式

551 阅读8分钟

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

观察者模式概述

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

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

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

观察者模式代码实现

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

/**
 * 主题
 */
public interface Subject {
    /**
     * 注册观察者
     * @param o 观察者对象
     */
    void registerObserver(Observer o);

    /**
     * 移除观察者
     * @param o 观察者对象
     */
    void removeObserver(Observer o);

    /**
     * 通知观察者
     */
    void notifyObserver();
}

天气数据就是一个主题,因此定义出天气主题的类:

import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
    // 存储了此主题的观察者
    private List<Observer> observers;

    // 温度
    private float temp;
    // 湿度
    private float humidity;
    // 气压
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int index = observers.indexOf(o);
        if(index >= 0) observers.remove(index);
    }

    @Override
    public void notifyObserver() {
        for(Observer o: observers){
            o.update(temp, humidity, pressure);
        }
    }

    /**
     * 从气象站得到更新的观测值,通知观察者
     */
    public void measurementsChanged(){
        notifyObserver();
    }

    /**
     * 气象站设置新的值
     */
    public void setMeasurements(float temp, float humidity, float pressure){
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

上面的观察者还未定义呢,还是先定义一个统一的观察者数据更新方法的接口

/**
 * 观察者
 */
public interface Observer {
    /**
     * 更新展示板
     * @param temp 温度
     * @param humidity 湿度
     * @param pressure 气压
     */
    void update(float temp, float humidity, float pressure);
}

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

public interface DisplayElement {
    void display();
}

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

/**
 * 布告板
 */
public class CurrentConditionDisplay implements Observer, DisplayElement{
    private float temp;
    private float humidity;

    public CurrentConditionDisplay(Subject weatherData) {
        weatherData.registerObserver(this);
    }

    public void stopDisplay(Subject weatherData){
        weatherData.removeObserver(this);
    }

    @Override
    public void display() {
        System.out.println(temp + "℃ " + humidity + "%");
    }

    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temp = temp;
        this.humidity = humidity;
        display();
    }
}

接下来测试一下写的观察者模式:

public class Test {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        // 把显示装置注册到WeatherData的观察者列表
        CurrentConditionDisplay display = new CurrentConditionDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(85, 70, 32.2f);
        weatherData.setMeasurements(86, 72, 36.3f);
        weatherData.setMeasurements(89, 76, 38.0f);

        System.out.println("==========================================");

        // 停止观察
        display.stopDisplay(weatherData);
        weatherData.setMeasurements(91, 77, 39.5f);
        weatherData.setMeasurements(97, 79, 40.2f);
    }
}

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() 方法一样,只是方法参数略有不同:

update(Observable o, Object arg)

第一个参数 Observable 就是主题对象,好让观察者知道是哪个主题通知它的;第二个参数就是上面的例子中的参数,即数据对象。

如果使用推模式,则可以把数据当做数据对象传入 notifyObservers(Object arg) 中。否则观察者就必须从被观察者对象中拉取数据,我们把上面气象站的例子重做一次。

4、关于 setChanged()

setChanged() 方法用于标记状态已经改变的事实,好让 notifyObservers() 知道当它被调用时就应该更新观察者。如果调用 notifyObservers() 之前没有调用 setChanged(),则观察者不会被通知,伪代码如下:

setChaged(){
    changed = true
}

notifyObservers(Object arg){
    if(changed){
        for obs in obsList {
            call update(this, arg)
        }
        changed = false
    }
}

notifyObservers(){
    notifyObservers(null)
}

这样做的目的就是在更新观察者的时候能有更多的弹性,比如在你想在气象温度变化 0.5 度以上才通知观察者,就需要通过调用 setChanged 这样的方式进行数据的有效更新。

JDK内置观察者重做气象站

WeatherData.java

import java.util.Observable;

public class WeatherData extends Observable {
    // 温度
    private float temp;
    // 湿度
    private float humidity;
    // 气压
    private float pressure;

    public WeatherData() { }

    public void measurementsChanged(){
        setChanged();
        notifyObservers();
    }

    /**
     * 气象站设置新的值
     */
    public void setMeasurements(float temp, float humidity, float pressure){
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    // 观察者会利用这些Getter方法取得WeatherData的状态
    public float getTemp() {
        return temp;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

CurrentConditionDisplay.java (DisplayElement 和前面的例子一样)

import java.util.Observable;
import java.util.Observer;

public class CurrentConditionDisplay implements Observer, DisplayElement {
    private Observable observable;
    private float temp;
    private float humidity;

    public CurrentConditionDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void display() {
        System.out.println(temp + "℃ " + humidity + "%");
    }

    @Override
    public void update(Observable o, Object arg) {
        //System.out.println("被观察者:" + o.getClass().getName());
        if(o instanceof WeatherData){
            WeatherData weatherData = (WeatherData) o;
            this.humidity = weatherData.getHumidity();
            this.temp = weatherData.getTemp();
            display();
        }
    }
}

主题对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。一般这种模型的实现中,会把主题对象自身通过 update() 方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

观察者模式的优缺点

1、优点

首先是松耦合,当两个对象之间松耦合,它们依旧可以交互,但是不太清楚彼此的细节,观察者模式就提供了这样一种对象设计,让主题和观察者之间松耦合。

主题值需要知道观察者实现了某个接口,也就是 Observer 接口,不需要知道具体观察者实现类是什么,也不用关系观察者的实现细节。任何时间我们都可以动态的添加或者移除观察者、也包括替换新的观察者等操作,主题都不会受到影响。改变被观察者和观察者任意一方都不会影响另一方,这就是松耦合特点。

2、缺点

接下来说说缺点, 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间,也就是说同一个主题的观察者不能太多,太多了每次通知都是需要消耗时间的。

而且如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。

接下来讨论一个问题,但是却不是观察者模式的问题,而是 JDK 内置的观察者模式的问题。java.util.Observable 是一个类而不是一个接口,如果要使用必须继承这个类,这其实限制了 Observable 的复用能力,而且通过源码可以看到 setChanged()是受保护的权限,这意味着只能继承 java.util.Observable,这违反了 “多用组合、少用继承” 的原则。平时使用的时候应该多注意这个问题,有必要的话最好自己实现一套观察者模式。

参考资料

《Head First 设计模式》