设计模式(二) - 观察者模式(Observer Pattern)

一、什么是观察者模式?

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

[观察者模式-类图]


观察者模式(Observer Pattern) = 主题(Subject) + 观察者(Observer)

● Subject(主题):主题又称为目标,它是指被观察的对象。在主题中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notifyObservers()。目标类可以是接口,也可以是抽象类或具体类(推荐使用接口,这样符合我们上一篇策略模式中提到的设计原则:多用组合少用继承;同时可以达到松耦合的目的)。

+

● ConcreteSubject(具体主题):具体主题是主题类的实现类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。

● Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。

● ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体主题对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的registerObserver()方法将自己添加到主题类的集合中,或者通过removeObserver()方法将自己从主题类的集合中删除。


二、代码示例

我们举一个气象站的例子:创建一个WeatherData对象(追踪来自气象站的数据,并更新布告板),和三个布告板(显示目前天气状况:温度、湿度、气压给用户看),一旦WeatherData对象有新的测量,这些布告必须马上更新。我们采用观察者模式来实现这个程序,并用WeatherData对象来实现Subject接口,当他来当“主题”,让布告板来实现Observer接口,来当我们的“观察者”。

接口包:

/**
 * 主题接口
 */
public interface ISubject {
    /**
     * 注册观察者
     * @param o 观察者
     */
    void registerObserver(IObserver o);

    /**
     * 移除观察者
     * @param o 观察者
     */
    void removeObserver(IObserver o);

    /**
     * 通知观察者
     */
    void notifyObservers();
}
/**
 * 观察者接口
 */
public interface IObserver {
    /**
     * 公共的更新方法
     * @param temperature 温度
     * @param humidity 湿度
     * @param pressure 气压
     */
    void update(float temperature, float humidity, float pressure);
}
/**
 * 用来展示的一个公共接口
 */
public interface IDisplayElement {
    void display();
}

具体的主题:

/**
 * 具体的主题
 */
public class WeatherData implements ISubject {
    private List<IObserver> observers;  // 用一个List来记录观察者
    private float temperature;  // 温度
    private float humidity;     // 湿度
    private float pressure;     // 气压

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

    /**
     * 注册观察者
     * @param o 观察者
     */
    @Override
    public void registerObserver(IObserver o) {
        if (o == null)
            throw new NullPointerException();
        observers.add(o);
    }

    /**
     * 注销观察者
     * @param o 观察者
     */
    @Override
    public void removeObserver(IObserver o) {
        int i = observers.indexOf(o);
        if (i >= 0)
            observers.remove(i);
    }

    /**
     * 通知观察者
     */
    @Override
    public void notifyObservers() {
        // 这里我们把状态告诉每一个观察者。因为观察者都实现了update()方法,所以我们知道如何通知他们。
        for (IObserver o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }

    /**
     * 当WeatherData从气象站得到更新观测值时,我们通知观察者
     */
    private void measurementsChanged() {
        notifyObservers();
    }

    // 用这个set值得方法来测试布告板
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
具体的观察者们:

/**
 * 此布告板根据WeatherData对象显示当前观测值
 */
public class CurrentConditionsDisplay implements IObserver, IDisplayElement {
    private float temperature;  // 温度
    private float humidity;     // 湿度
    private ISubject weatherData;

    public CurrentConditionsDisplay(ISubject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

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

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature
                + "F degrees and " + humidity + "% humidity");
    }
}
/**
 * 此布告板根据气压计显示天气预报
 */
public class ForecastDisplay implements IObserver, IDisplayElement {
    private float currentPressure = 29.92f;
    private float lastPressure;
    private WeatherData weatherData;

    public ForecastDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        lastPressure = currentPressure;
        currentPressure = pressure;

        display();
    }

    @Override
    public void display() {
        System.out.print("Forecast: ");
        if (currentPressure > lastPressure) {
            System.out.println("Improving weather on the way!");
        } else if (currentPressure == lastPressure) {
            System.out.println("More of the same");
        } else {
            System.out.println("Watch out for cooler, rainy weather");
        }
    }
}
/**
 * 此布告板跟踪最小、平均、最大的观测值,并显示
 */
public class StatisticsDisplay implements IObserver, IDisplayElement {
    private float maxTemp = 0.0f;
    private float minTemp = 200;
    private float tempSum = 0.0f;
    private int numReadings;
    private WeatherData weatherData;

    public StatisticsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        tempSum += temperature;
        numReadings++;

        if (temperature > maxTemp) {
            maxTemp = temperature;
        }

        if (temperature < minTemp) {
            minTemp = temperature;
        }

        display();
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
                + "/" + maxTemp + "/" + minTemp);
    }
}
气象站测试类:

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        new CurrentConditionsDisplay(weatherData);
        new StatisticsDisplay(weatherData);
        new ForecastDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}
让我们看一下输出结果:

Current conditions: 80.0F degrees and 65.0% humidity
Avg/Max/Min temperature = 80.0/80.0/80.0
Forecast: Improving weather on the way!
Current conditions: 82.0F degrees and 70.0% humidity
Avg/Max/Min temperature = 81.0/82.0/80.0
Forecast: Watch out for cooler, rainy weather
Current conditions: 78.0F degrees and 90.0% humidity
Avg/Max/Min temperature = 80.0/82.0/78.0
Forecast: More of the same
正如我们所见,我们在setMeasurements()方法里更新了WeatherData的数据(相当于我们更改了主题的状态),观察者们就自动收到了来自主题的通知(我们将新的状态都用display()给打印了出来),每一次主题更新,观察者们就会收到新的推送。这与我们最初的设计意图相符,并且体现出了观察者模式的神奇功效。

二、观察者的优缺点

到目前为止,我们基本实现了一个较为完整的观察者模式并且见证了它的神奇功效,那么观察者模式有什么优缺点呢?接下来让我们对观察者模式进一步的思考。

优点:

  • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
  • 观察者模式在观察目标和观察者之间建立一个抽象的耦合。
  • 观察者模式支持广播通信。
  • 观察者模式符合“开闭原则”的要求。
缺点:

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

在观察者模式中,又有推(push)或拉(pull)两种方式来传送数据。

  • 推模式

 主题对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。

  •  拉模式

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

其实在JDK中,Java已经内置了对观察者模式的支持,在java.util包内包含最基本的Observer接口和Observable类(也就是我们的Subject主题),使用内置的API更为方便,让我们来分析一下JDK中的观察者API:

Observer观察者:

public interface Observer {
    /**
     * 
     * @param   o     主题本身,好让观察者知道是哪个主题通知它的
     * @param   arg   传入的数据对象,通过这个参数可以实现拉(pull)或者推(push)数据
     */
    void update(Observable o, Object arg);
}

Observable可观察者:

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;

    public Observable() {
        obs = new Vector<>();
    }

    /**
     * 用于注册新的观察者对象
     * @param   o   an observer to be added.
     * @throws NullPointerException   if the parameter o is null.
     */
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    /**
     * 用于删除向量中的一个观察者
     * @param   o   the observer to be deleted.
     */
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }

    /**
     * 通知观察者
     */
    public void notifyObservers() {
        notifyObservers(null);
    }

    /**
     * 多了一个对象参数,此方法可以传送任何数据对象给每一个观察者
     */
    public void notifyObservers(Object arg) {
        Object[] arrLocal;

        synchronized (this) {
            // 只有在changed标识为true的时候才会通知观察者
            if (!changed)
                return;
            arrLocal = obs.toArray();
            // 通知完之后会把changed标识设回为false
            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标识设为false
     */
    protected synchronized void clearChanged() {
        changed = false;
    }

    /**
     * 返回changed标识的状态
     */
    public synchronized boolean hasChanged() {
        return changed;
    }

    /**
     * 返回观察者的shu'liang
     */
    public synchronized int countObservers() {
        return obs.size();
    }
}
我们可以看到,Observer(观察者)中的update方法多加了个一个可以传递数据的参数args,并且将主题也传入了进来,这样可以让观察者知道是哪个主题通知它的。而Observable(可观察者,也就是前面我们说的主题)基本上与我们前面的实现类似,但是多了一个setChanged()方法,以及一个changed标识,该方法用来标记状态已经改变的事实,好让notifyObservers()知道它被调用时应该更新观察者。如果调用notifyObservers()之前没有先调用setChanged(),观察者就不会被通知。

这样做是有其必要性的,这样可以让我们在更新观察者时,有更多的弹性,我们可以适当地通知观察者。比如,如果没有这个setChanged(),那么我们的气象站测量时,变化是十分敏锐的,温度计读书每十分之一度就会更新,这会造成WeatherData对象持续不断地通知观察者,这可不是我们希望看到的。

下面我们来看一下用JDK内置的API实现的气象站:

可观察者(WeatherData):

/**
 * 利用JDK里提供的类来实现观察者模式
 */
public class WeatherData extends Observable {
    private float temperature;  // 温度
    private float humidity;     // 湿度
    private float pressure;     // 气压

    public WeatherData() { }

    public void measurementsChanged() {
        setChanged();   // 在调用notifyObservers()前先调用setChanged()来指示状态已经改变
        notifyObservers();  // 没有写入参数,表明我们没有传数据,在此采用的是 拉(pull)
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

观察者们:

/**
 * 观察者1:实时显示数据的告示板
 */
public class CurrentConditionsDisplay implements Observer, IDisplayElement {
    private Observable observable;
    private float temperature;
    private float humidity;

    /**
     * 传入了一个可观察者当参数,并将当前对象注册成观察者
     * @param observable 主题(可观察者)
     */
    public CurrentConditionsDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        // 先确定观察者是否属于WeatherData类型,然后利用getter方法获取温度和湿度,之后调用display()
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature
            + "F degrees and " + humidity + "% humidity");
    }
}
/**
 * 观察者2:此布告板根据气压计显示天气预报
 */
public class ForecastDisplay implements Observer, IDisplayElement {
    private float currentPressure = 29.92f;
    private float lastPressure;

    public ForecastDisplay(Observable observable) {
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            lastPressure = currentPressure;
            currentPressure = weatherData.getPressure();
            display();
        }
    }

    @Override
    public void display() {
        System.out.print("Forecast: ");
        if (currentPressure > lastPressure) {
            System.out.println("Improving weather on the way!");
        } else if (currentPressure == lastPressure) {
            System.out.println("More of the same");
        } else if (currentPressure < lastPressure) {
            System.out.println("Watch out for cooler, rainy weather");
        }
    }
}
/**
 * 此布告板跟踪最小、平均、最大的观测值,并显示
 */
public class StatisticsDisplay implements Observer, IDisplayElement {
    private float maxTemp = 0.0f;
    private float minTemp = 200;
    private float tempSum = 0.0f;
    private int numReadings;

    public StatisticsDisplay(Observable observable) {
        observable.addObserver(this);
    }

    @Override
    public void update(Observable observable, Object arg) {
        if (observable instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) observable;
            float temp = weatherData.getTemperature();
            tempSum += temp;
            numReadings++;

            if (temp > maxTemp) {
                maxTemp = temp;
            }

            if (temp < minTemp) {
                minTemp = temp;
            }

            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
                + "/" + maxTemp + "/" + minTemp);
    }
}
气象站测试类:

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        new CurrentConditionsDisplay(weatherData);
        new ForecastDisplay(weatherData);
        new StatisticsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}
测试结果:

Avg/Max/Min temperature = 80.0/80.0/80.0
Forecast: Improving weather on the way!
Current conditions: 80.0F degrees and 65.0% humidity
Avg/Max/Min temperature = 81.0/82.0/80.0
Forecast: Watch out for cooler, rainy weather
Current conditions: 82.0F degrees and 70.0% humidity
Avg/Max/Min temperature = 80.0/82.0/78.0
Forecast: More of the same
Current conditions: 78.0F degrees and 90.0% humidity
我们可以看到,结果与之前除了顺序之外,没有什么不同,说明实现是成功了的。但是为什么会这样呢?其实原因就在java.util.Observable实现的notifyObservers()方法,中间是采取的倒序update(),这不同于我们之前的次序。谁也没有错,只是双方选择的方式不同罢了。

但是可以肯定,我们的代码依赖这样的次序,就是有问题的。为什么呢?因为一旦观察者/可观察者的实现有所改变,通知次序就会改变,很可能就会产生错误的结果,这绝不是我们所认为的松耦合。

让我们来看看API中的Observable的黑暗面:

1.Observable是一个 “类”,而不是一个“接口”,更糟的是,他甚至没有实现一个接口。这样的实现限制了它的使用和复用,虽然它的功能性并没有问题,但是不符合我们上一篇中所提到的另一个设计原则:针对接口编程,而不是针对实现编程

2.Observable将关键的方法保护起来了:我们可以看到API中,setChanged()方法被标为了protected,这意味着:除非你继承自Observable,否则你无法创建实例并组合到自己的对象中来,这又违反了之前提到的设计原则:多用组合,少用继承


总结:

1.观察者模式定义了对象之间的一对多关系;

2.观察者和可观察者之间用松耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者的接口;
3.使用此模式时,你可以从被观察者处推(push)或拉(pull)数据(然而,推的方式被认为你更“正确”);

4.有多个观察者时,不可以依赖特定的通知次序;

5.Java中有多重观察者模式,包括了通用的java.util.Observable;

6.我们要注意java.util.Observable实现上带来的一些问题;

7.如果有必要的话,可以实现一个自己的Observable(正如我们之前所做的,创建一个Subject接口),这并不难,也不复杂;

8.补充本章的一条设计原则:为交互对象之间的松耦合设计而努力(松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低)。


ps: 如有不同的见解,可以留言,如要转载,请标明出处谢谢。

下一篇将对“装饰者模式”做介绍。

猜你喜欢

转载自blog.csdn.net/siny_chan/article/details/79186832