框架原理设计模式设计模式

设计模式之观察者(Observer)模式

2019-04-21  本文已影响275人  纸中圆

什么是观察者模式?

  观察者模式软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。.

  该模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事。对象甚至在运行时可决定是否要继续被通知。观察者模式是JDK中使用最多的模式之一,非常有用。

认识观察者模式

  我们看看B站用户的关注是怎么回事:

①up主的任务是更新视频。
②用户关注某个up主后,只要他们发布了新视频,就会给你发送一条动态消息。只要你是up主的粉丝,你就会一直收到新的动态消息。
③有一天你发现你对这个up主不感兴趣了,不想再看这个up主,取消关注,你就不会再收到那个up主的动态消息了。
④只要up主一直更新视频,就会有人关注他,毕竟这莫名的时代你直播修床都会有人看,就是这么无聊。。。

出版者(up主)+订阅者(粉丝)=观察者模式

  如果你了解B站的订阅是怎么回事了,其实就知道观察者模式是怎么回事,只是名称不太一样:出版者改称为“主题”(Subject),订阅者改称为“观察者”(Observer)。具体如下图:

观察者模式的日常行为

  有一天突然跑过来告诉主题,你想当一个观察者。其实想说的是:我对你的数据感兴趣,一有变化请通知我,如下图:


  当主题对象同意了的申请,你就成为正式的观察者了。现在可以静候通知,等着新的视频发布。一旦接收到新通知,就会得到一个数据。
订阅主题后
  这样主题也有了新数据(粉丝数+1)!其他观察者粉丝也会收到通知:主题已经改变了(你关注的up主又涨粉丝了)。

  当有一天,1号粉丝对象要求从观察者中把自己除名,1号粉丝已经观察此主题太久,不想关注这个up主了,厌倦了,所以决定不再当个观察者。如下图所示:



  1号粉丝对象脱粉了!主题对象知道1号粉丝的请求之后,把它从观察者中除名,如下图:


1号粉丝对象脱粉
  主题有了新的数据(粉丝数-1)。除了1号粉丝之外,每个观察者都会收到通知(up主粉丝-1)。

定义观察者模式

  当你试图回想起观察者模式时,可以想想华农吃竹鼠的视频,你曾经在B站订阅过他,你是订阅者,他是出版者。因此观察者模式被定义成:

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

一对多关系

  主题和观察者定义了一对多的关系。观察者依赖于此主题,,只要主题状态一有变化,观察者就会被通知。根据通知的风格,观察者可能因此新值而更新。
  实现观察者模式的方法不只一种,但是以包含SubjectObserver接口的类设计的做法最常见。类图如下:


  具体的主题是具有状态变量的对象,并且可以控制这些状态变量。也就是说, 有“一个”具有状态变量的主题。另一方面,观察者使用这些状态变量,虽然这些状态变量并不属于他们。有许多的观察者,依赖主题来告诉他们状态何时改变了。这就产生一个关系:“一个”主题对“多个”观察者的关系。
  因为主题是真正拥有数据的人,观察者是主题的依赖者,在数据变化时更新,这样比起让许多对象控制同一份数据来,可以得到更干净的的OO设计。

作用:松耦合

  观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
  为什么呢?
  关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主 题不需要知道观察者的具体类是谁、做了些什么或其他任何细节。
  任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可 以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何时候删除某些观察者。
  有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当 观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里 实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所 有实现了观察者接口的对象。
  我们可以独立地复用主题或观察者。如果我们在其他地方需要使用主题或观察者,可 以轻易地复用,因为二者并非紧耦合。
  改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他 们之间的接口仍被遵守,我们就可以自由地改变他们。
  设计原则:为了交互对象之间的松耦合设计而努力。
  松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化, 是因为对象之间的互相依赖降到了最低。

通过气象观测应用了解观察者模式

  现在需要你写一个气象观测应用,该应用由WeatherData对象(可观察的主题对象)负责追踪目前的天气状况(温度、湿度、气压),且有3个布告板(观察者对象)分别显示目前的状况(温度、湿度、气压)、气象统计(平均温度,最高低温度)及简单的预报(晴、雨、雪等)。当WeatherObject对象(主题对象)获得最新的测量数据时,将注册了WeatherObject对象(可观察的主题对象)的三种布告板(观察者对象)实时更新。

理清思路

  此系统中的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看),如下图:


  WeatherData对象知道如何跟物理气象站联系(),以取得更新的数据。数据改变后WeatherData对象会随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。那么WeatherData对象应该有下面的特点:

  那么我们如何使用观察者模式实现这个应用呢?我们知道观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。此处,我们的WeatherData类正是此处所说 的“一”,而我们的“多”正是使用天气观测的各种布告板对象。
  WeatherData对象的确是有状态,包括了温度、湿度、气压,而这些值都会改变,当这些观测值改变时,必须通知所有的布告板,好让它们各自做出处理。
  如果我们把WeatherData对象当作主题,把布告板当作观 察者,布告板为了取得信息,就必须先向WeatherData对象注册。一旦WeatherData知道有某个布告板的存在,就会适时地调用布告板的某个 方法来告诉布告板观测值是多少。
  每个布告板都有差异,这也就是为什么我们需要一个共同的接口的原因。尽管布告板的类都不一样,但是它们都应该实现相同的接口,好让WeatherData对象能够知道如何把观测值送给它们。所以每个布告板都应该有一个大概名为update()的方法,以供 WeatherData对象调用。
  因此,设计图如下:

实现气象站

  有了设计图,我们就可以正式编码了。首先,我们建立3个接口:

//主题接口
public interface Subject {
    //注册观察者
    void registerObserver(Observer o);
    //移除观察者
    void removerObserver(Observer o);
    //通知所有观察者
    void notifyObservers();
}

//观察者接口
public interface Observer {
    //当气象观测值改变时,主题会把这些状态值当作 方法的参数,传送给观察者,
    //因为把观测值直接传入观察者不明智,这些观测值的种类和数量未来可能会变化,所以后期还会优化该类
    void update(float temperature,float humidity,float pressure);
}

//具体信息显示接口
public interface DisplayElement {
    //当布告板需要显示时, 调用此方法。
    void display();
}

  编写主题接口的实现类:

import ...
public class WeatherData implements Subject {
    //观察者集合
    private List observers;
    //湿度
    private float temperature;
    //温度
    private float humidity;
    //气压
    private float pressure;


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

    //注册观察者
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    //移除观察者
    @Override
    public void removerObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    //通知所有观察者
    @Override
    public void notifyObservers() {
        for (int i = 0; i < observers.size(); i++){
            Observer observer = (Observer) observers.get(i);
            observer.update(temperature,humidity,pressure);
        }
    }

    //测量结果改变则调用通知所有观察者方法
    public void measurementsChanged(){
        notifyObservers();
    }

    //模拟手动设置测量结果(湿度,温度,气压)
    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

  编写观察者接口的实现类之一(当前状况):

public class CurrentConditionsDisplay implements Observer, DisplayElement {
    //湿度
    private float temperature;
    //温度
    private float humidity;
    //气压
    private float pressure;
    //天气数据对象
    private Subject weatherData;

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

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

    @Override
    public void display() {
        System.out.println("目前状况:湿度为" + temperature + "RH; 温度为:" + humidity + "度;气压为" + pressure + "帕斯卡");
    }
}

  编写测试类:

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

        CurrentConditionsDisplay ccd = new CurrentConditionsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

  测试类运行结果:

目前状况:湿度为80.0RH; 温度为:65.0度;气压为30.4帕斯卡
目前状况:湿度为82.0RH; 温度为:70.0度;气压为29.2帕斯卡
目前状况:湿度为78.0RH; 温度为:90.0度;气压为29.2帕斯卡

主题和观察者间的状态差异

  从前面一个例子中我们可以看到,当主题获得新的数据时,立马“推出”自己的状态信息给观察者,让观察者知道主题的最新情况。
  但有时候观察者并不是想立即知道主题的情况,比如观察者在忙重要的事情。那么,主题既然可以通过主动“推出”数据,是否也能让观察者主动去向主题去“拉取”数据呢?这当然是可以的,然而主题却不应该公开它所有的状态数据,这样太危险了,可以折中一下让主题选择性的公开一部分数据,提供getter方法即可,这样观察者就可以“拉取”走自己所需的状态数据。
  或许会有人觉得这样不方便,还要每次都手动去取主题的状态数据,这要调集好多次才能收集想要的状态数据。然而众口难调,不管是主题主动“推出”状态数据,还是观察者主动想主题“拉取”状态数据,都有各自的优缺点。
  Java内置的观察者模式对上面的两种做法都支持。

Java内置的观察者模式

  Java API有内置的观察者模式。java.util包内包含最基本的Observable类与Observer接口,这和我们的Subject接口与Observer接口很相似。 Observable类与Observer接口使用上更方便, 因为许多功能都已经事先准备好了。你甚至可以使用推(push)或拉(pull)的方式传送数据。
  为了更了解java.uitl.Observerjava.util.Observable,看看下面的图,这是修改后的气象应用设计:

运作方式分析

  Java内置的观察者模式运作方式,和我们在气象站中的实现类似,但有一些小差异。最明显的差异是WeatherData(也就是我们的主题)现在扩展自Observable类,并继承到一些增加、删除、通知观察者的方法(以及其他的方法,Java版本的用法步骤如下:

部分源码分析

  其中的setChanged()方法用来标记状态已经改变的事实,好让notifyObservers()知道当它被调 用时应该更新观察者。如果调用notifyObservers()之前没有先调用setChanged(),观察者就“不会”被通知。从Observable源码分析了解下:

    private boolean changed = false;
    //setChanged()方法把changed标志设 为true。
    protected synchronized void setChanged() {
        changed = true;
    }
    protected synchronized void clearChanged() {
        changed = false;
    }
    //notifyObservers() 只会在 changed标为“true”时通知观 察者。
    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);
    }

  通过setChanged()方法可以让你在更新观察者时,更适当地通知观察者,这样会使程序有更多的弹性。比如,如果没有setChanged()方法,气象站测量太过精确, 以致于温度计读数每十分之一度就会更新,这会造成WeatherData对象持续不断地通知观察者,浪费了资源。如果我们希望半度以上才更新,就可以在温度差距到达半度时,调用setChanged()(加个if语句判断即可)进行有效的更新。 或许我们不会经常用到此功能,但是把这样的功能准备好,需要时就可以马上使用。如果此功能在某些地方对你有帮助,你可能也需要clearChanged()方法,将changed状态设置回false。另外也有一个hasChanged()方法, 告诉你changed标志的当前状态。

改造原有气象站

  首先,我们让WeatherData对象继承Java自带的Observablel类再进行改造:

import java.util.Observable;

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

    public WeatherData() {
    }

    //测量结果改变时,只有先将changed状态改为true时,通知所有观察者方法才会被调用
    public void measurementsChanged(){
        setChanged();
        notifyObservers();
    }

    //模拟手动设置测量结果(湿度,温度,气压)
    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;
    }
}

  之后重做CurrentConditionsDisplay对象:

import java.util.Observer;

public class CurrentConditionsDisplay implements Observer, DisplayElement {
    //湿度
    private float temperature;
    //温度
    private float humidity;
    //气压
    private float pressure;
    //
    private Observable observable;

    //运用多态传入其Observable子类对象即可
    public CurrentConditionsDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }
    
    //现在自己去主题对象中拿数据
    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData){
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("目前状况:湿度为" + temperature + "RH; 温度为:" + humidity + "度;气压为" + pressure + "帕斯卡");
    }
}

  运行测试类:

        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay ccd = new CurrentConditionsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);

  测试结果:

目前状况:湿度为80.0RH; 温度为:65.0度;气压为30.4帕斯卡
目前状况:湿度为82.0RH; 温度为:70.0度;气压为29.2帕斯卡
目前状况:湿度为78.0RH; 温度为:90.0度;气压为29.2帕斯卡

  可以看到,测试结果和原来一样,只是内部定义不同。原来是自己定义的主题去“推送”数据给所有观察者,现在是用Java提供的类接口用观察者去“拉取”主题的数据(当然Java提供的也能推送数据,这里那样实现罢了)。
  当然Java自带的观察者模式类和接口也有缺点,比如主题Observable是一个“类”而不是一个接口,我们必须设计一个子类来实现它,如果该子类还要继承其他的父类,就无法做到了,因为Java不支持多继承,这限制了Observable的复用能力。而且setChanged方法被用protected关键字保护起来了,只有你继承了Observable类才能使用,无法将其组合到自己的对象中,违反了设计原则:多用组合,少用继承

抉择:使用自定义还是Java提供的观察者模式?

  如果能扩展Java自带的观察者模式API,它可能符合我们的需求,否则的话我们还是乖乖地向最开始一样实现一整套观察者模式。

实现up主和粉丝

  假如由你来设计B站的up主(Uploader)和粉丝(Fans)之间的关系,由Uploader对象负责接收相关信息(如up主姓名,所属分类,发布的视频信息),当Uploader对象一发布新的视频时,Fans对象就将收到一条新动态(你关注的up主某某更新了新的视频。。)当然,观察者可能不止粉丝一种,如果有赞助商,也可以算观察者(比如经常有在视频中拉赞助的。。。)
  此系统中的三个部分是up主(可以填写实际的数据)、Uploader对象(接收来自up主提供的数据)和粉丝(显示Uploader对象更新的动态)。
注意:此处up主和Uploader对象代表的意义不同,一个是现实生活中的实体,一个是虚拟的对象。就好像人(up主)可以操纵程序,向其写入数据,因此程序(Uploader对象)只相当于一个接收者。也就是说,Uploader对象依赖于人(up主)提供的数据。
  Uploader对象知道如何跟up主建立联系,以获取更新的数据。Uploader对象会随即更新观察者(粉丝、赞助商)的动态。
  所以Uploader对象应该3个getter方法,可以获取up主名字,up主类别,发布的视频信息,还应该有一个upStateChanged方法,当up主更新了新的数据时,该方法就会被调用。
  我们需要实现订阅了up主的粉丝对象或赞助商对象,一旦Uploader对象有了新的数据,这2个观察者就获得了新的动态。
  此系统可以扩展让其他开发任意定制新的观察者
  那么,我们该如何建立这个系统?当然是使用观察者模式了。请自己实现一下吧

参考资料

《HeadFirst设计模式》

上一篇 下一篇

猜你喜欢

热点阅读