观察者模式

126 阅读8分钟

提出需求

这里我们依旧参考 HeadFirst 设计模式 书中提出的问题,假设要建立一个气象站的公告板,现在已经为你提供了一些API可以调用 :

public float getTemperature();
public float getHumidity();
public float getPressure();

利用这些API你可以快速的从气象站中获取数据,我们可以为每一块布告板实现一个update()用来更新数据的功能,这样就可以简单的实现数据的实时更新

public void measurementsChange(){
    float temp = getTemperature();
    float humidity = getHumidity();
		float pressure = getPressure();
    
    currentConditionDisplay.update(temp, humidity, pressure);
    statisticsDisplay.update(temp, humidity, pressure);
    forecastDisplay.update(temp, humidity, pressure);
}

建立一个改变测量数据时调用的函数,每次调用都从get方法中获取到实时的值,然后将他update到每一块布告板中,但是这样的设计使我在增添删除布告板时,必须对measurementChanged方法进行更改。

观察者模式

为了解决上面的设计问题,我们可以依靠观察者模式。观察者模式建立一种对象与对象之间的依赖关系,当一个对象改变时自动通知其他对象,其他对象会就此做出反应。发生改变的对象称为观察目标,被通知的对象称为观察者,一个目标可以对应多个观察者,每个观察者也可以观察多个目标,并且可以自由的订阅和取消。这有点像我们在出版社订阅报纸,也可以叫做发布-订阅(Publish/Subscribe)模式。

被观察者

首先我们定义一个主题 (被观察者) 接口,主题应该有管理观察者的功能,就像报社需要对订报的客户进行管理一样。

interface Subject {

    /**
     * 注册观察者
     */
    void registerObserver(Observer observer);

    /**
     * 删除观察者
     */
    void removeObserver(Observer observer);

    /**
     * 通知所有观察者
     */
    void notifyObservers(ArrayList<Observer> observers);
}

具体观察对象

接下来我们就可以建立这个气象站这个具体的被观察对象,具体的被观察对象需要以下几点

  • 发送给观察者的对象
  • 拥有一个观察者名单进行管理
  • 具体的管理方法
  • 数据改变时的发生的操作
/**
 * 天气数据
 */
public class WeatherData implements Subject {

    /**
     * 天气数据
     */
    private float temperature;
    private float humidity;
    private float pressure;

    /**
     * 通过一个ArrayList来管理需要通知的观察者
     */
    private final ArrayList<Observer> observers;

    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);
            System.out.println("remove");
        }
    }

    /**
     * 通知观察者
     */
    @Override
    public void notifyObservers(ArrayList<Observer> observers) {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    /**
     * 当天气数据发生修改
     */
    private void measurementsChanged() {
        notifyObservers(observers);
    }

    /**
     * 设置测量值
     * 测量值设置后启用 天气数据发生修改 的方法
     */
    void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

观察者接口

然后我们需要定义一个观察者的接口,他需要有更新自身数据的功能,在观察者模式中,将工作尽量交给主题(报社)来处理,让每个订阅者只需更新数据即收到的信息即可。这里我们的订阅的时天气数据,因此更新收到的温度、湿度、气压数据。

/**
 * 观察者
 */
interface Observer {

    /**
     * 更新数据
     */
    void update(float temp, float humidity, float pressure);
}

具体的观察者

公告牌需要更新得到的数据,并且在显示上进行更新。

  • 注册成为观察者
  • 保留对主题的引用方便以后的修改 -> 读者自己取消订阅
  • 更新数据
/**
 * 天气展示板 观察者之一
 */
public class CurrentConditionDisplay implements Display, Observer {

    /**
     * 天气数据,仅展示温度和湿度
     */
    private float temperature;
    private float humidity;
    /**
     * 要订阅的主题
     */
    private final Subject weatherData;

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

    @Override
    public void display() {
        System.out.println("temperature: " + temperature + ", humidity: " + humidity);
    }

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

这里我们实现了一个Display用作显示的接口

public interface Display {

    void display();
}

测试

public class WeatherStation {

    public static void main(String[] args) {

        // 设置一个订阅的主题
        WeatherData weatherData = new WeatherData();
        // 布告板
        CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
        // 天气数据变动
        weatherData.setMeasurements(18, 20, 10000);
        weatherData.setMeasurements(20, 30, 10200);
        // 取消订阅
        weatherData.removeObserver(currentConditionDisplay);
        // 天气数据变动
        weatherData.setMeasurements(25, 46, 10060);
    }
}

测试结果

image.png 我们发现,使用了观察者模式后,表现层和数据层实现了分离,观察者模式定义了稳定的消息更新传递机制,消息有具体的被观察者来定义被决定如何传输,通过调用notify()方法来实现数据的传递,也抽象了更新接口,把观察者接受更新的接口封装在Observer接口的update()方法中。

使用Java内置的观察者模式

Java中内置了观察者模式,在 java.util 包中内置了Observer接口和Observable类,使用方法与Observer接口和Subject接口相似,但这个内置的观察者模式还有着一些上面我们没有实现的功能 —— 获取数据的主动权和知道数据来源的功能。

Observable类源码

/*
 * Copyright (c) 1994, 2012, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package java.util;

public class Observable {

    // changed时判断是否需要进行通知的标识
    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);
    }
    /**
     * notifyObservers()方法可以在具体的观察对象来自定义,无参数则通知全部
     */
    public void notifyObservers() {
        notifyObservers(null);
    }
    /**
     * notifyObservers()在有对应观察者作为参数时,只传给该观察者
     */
    public void notifyObservers(Object arg) {
        Object[] arrLocal;
        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }
    /**
     * 当要通知观察者时需要进行此操作
     */
    protected synchronized void setChanged() {
        changed = true;
    }

    protected synchronized void clearChanged() {
        changed = false;
    }

    public synchronized boolean hasChanged() {
        return changed;
    }

    public synchronized int countObservers() {
        return obs.size();
    }
}

Observable超类中实现了对观察者的管理具体方法,也拥有了观察者登记表,这样使得我们在编写具体观察者时可以直接使用父类的方法进行基本的管理

Observer接口源码

/*
 * Copyright (c) 1994, 1998, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package java.util;

public interface Observer {
    void update(Observable o, Object arg);
}

与我们编写的Observer接口类似,但这里规定了一个观察对象参数和观察量参数,这样观察者就可以知道这是哪个观察对象传来的数据。

实现

首先我们实现具体的被观察者

  • 天气参数、当前参数
  • 参数发生变化调用的方法
public class WeatherData extends Observable{

    /**
     * 天气数据
     */
    private float temperature;
    private float humidity;
    private float pressure;

    /**
     * 实时的天气数据
     */
    private float realTemperature;
    private float realHumidity;
    private float realPressure;

    WeatherData() {
    }

    /**
     * 数据改变后measurementsChanged先被调用,判断是否更新当前数据,更新则然后执行setChange()
     *
     */
    void measurementsChanged() {
        // 温差不超过2度时不更新数据,避免数据频繁更新
        if (abs(realTemperature-this.temperature)>2) {
            // 将changed参数改为true,此时notifyObservers()内部可以被执行
            setChanged();
            System.out.println("changed");
            this.temperature = realTemperature;
            this.humidity = realHumidity;
            this.pressure = realPressure;
        } else {
            System.out.println("too Little");
        }
        // 遍历观察者列表进行update,在观察者中重写update
        notifyObservers();
    }

    /**
     * 用于测试的改变数据
     */
    void setMeasurements(float temperature, float humidity, float pressure) {
        this.realTemperature = temperature;
        this.realHumidity = humidity;
        this.realPressure = pressure;
    }

    /**
     * 对外提供获取数据的接口
     */
    float getTemperature() {
        return temperature;
    }

    float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

具体观察者

public class CurrentConditionDisplay implements Observer, Display {
    private float temperature;
    private float humidity;

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

    /**
     * 这里的update()方法与上面实现的不同,采用的是观察者主动向被观察者索取数据
     */
    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData)o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("temperature: " + temperature + ", humidity: " + humidity);
    }
}

测试

public class WeatherStation {
    
    public static void main(String[] args) {
        // 被订阅的主题:天气数据
        WeatherData weatherData = new WeatherData();
        // 展示板订阅这个天气主题
        CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
        // 设置天气变量
        weatherData.setMeasurements(25, 60, 10600);
        // 天气变量发生改变
        weatherData.measurementsChanged();
        weatherData.setMeasurements(29, 45, 10050);
        weatherData.measurementsChanged();
        weatherData.setMeasurements(30, 45, 10050);
        weatherData.measurementsChanged();
        // 取消订阅
        weatherData.deleteObserver(currentConditionDisplay);
        weatherData.setMeasurements(25, 60, 10600);
        weatherData.measurementsChanged();
    }
}

结果

image.png

总结

模式结构

UML图

image.png

时序图

image.png

设计原则

为了交互对象之间的松耦合设计而努力

  • 观察者模式提供了一种让两个对象实现松耦合的设计

主题只知道观察者实现了Observer接口,他唯一依赖的是Observer接口的对象列表,主题负责对Observer进行管理,向Observer推送数据,他不在乎谁实现了观察者接口,因此在运行时可以随时修改为新的观察者。新的类型的观察者出现时,不需要修改主题代码,只需要新实现一个观察者接口然后向主题注册。这种对主题和观察者的独立复用,源于他们之间松耦合的设计。

缺点

  • 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

应用环境

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。