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

什么是观察者模式?

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

  该模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事。对象甚至在运行时可决定是否要继续被通知。观察者模式是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类具有3个getter方法,可以取得三个测量值:温度、湿度与气压。
  • 当新的测量数据备妥时,该类的measurementsChanged()方法就会被调用(我们不在乎此方法是如何被调用的,我们只在乎它被调用了)。
  • 我们需要实现三个使用天气数据的布告板对象:“目前状况”布告、“气象统计”布告、“天气预报”布告。一旦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版本的用法步骤如下:

  • 把对象变成观察者:如同以前一样,实现观察者接口(java.uitl.Observer),然后调用任何Observable对象的addObserver()方法。不想再当观察者时,调用deleteObserver()方法就可以了。
  • 主题送出通知:首先,你需要利用扩展java.util.Observable接口产生“可观察者”类,然后,需要两个步骤:①先调用setChanged()方法,标记状态已经改变的事实,②然后调用两种notifyObservers()方法中的一个:notifyObservers()notifyObservers(Object arg)
  • 观察者接收通知:同以前一样,观察者实现了更新的方法,但是方法的签名不太一样:update(Observable o, Object arg),其中主题本身当作第一个变量, 好让观察者知道是哪个主题通知它的,第二遍变量arg传入notifyObservers()的数据对象。 如果没有说明则为空。
      如果你想“推”(push)数据给观察者,你可以把数据当作数据对象传送给 notifyObservers(arg)方法。否则,观察者就必须从可观察者对象中“拉”(pull)数据。 如何拉数据?我们再做一遍气象站,你很快就会看到。

部分源码分析

  其中的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设计模式》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容