观察者模式(从放弃到入门)

今天分享第二个模式,观察者模式。相信作Android开发或者Java开发的童鞋都听说过这个模式,而且有很多流行的框架都是使用了观察者模式,例如著名的RxJava。而且Java中直接就自带了观察者模式,可见它有多常用。

然后文中的例子也是来自 Head First 设计模式,很棒的一本书,推荐大家看看。

废话不多说了,直接上需求:

需求1:错误示范

系统中有3个东东:气象站(温度,湿地,气压等),WeatherData对象(获取各种消息的变化),广告板(显示变化)。直接上一张书上的图好了:

1.png

注意一定,公告板只是一种展示,可能还有其他功能的公告板,获取相同的数据,但是做不同的处理。图中是做目前的状态展示,还可能有气候统计天气预测等。
意思就是说:我们可以将 WeatherData对象看作一个消息的来源,它的各项参数会不停的变化,他的每一次变化,都需要通知广告板做出相应的处理。

那么最简单的设计就来了,既然后很多公告板,那么我的WeatherData对象中,存放他们的引用,然后当我的值变化的时候,依次调用公告板的change() 函数通知变化不久搞定了。代码如下:

WeatherData .java

public class WeatherData {
    
    CurrentConditionDisplay cDisplay;
    StaticsDisplay sDisplay;
    ForecastDisplay fDisplay;
    
    public WeatherData(){
        cDisplay = new CurrentConditionDisplay();
        sDisplay = new StaticsDisplay();
        fDisplay = new ForecastDisplay();
    }
    
    // 冲气象站获取到数据时执行更新
    public void measurementsChanged(){
        double temp = getTemperature();
        double humidity = getHumidity();
        double pressure = getPressure();
        
        cDisplay.update(temp, humidity, pressure);
        sDisplay.update(temp, humidity, pressure);
        fDisplay.update(temp, humidity, pressure);
    }
    
    public float getTemperature(){
        //TODO: 冲气象站获取温度
        return 1.0f;
    }
    
    public float getHumidity(){
        //TODO: 冲气象站获取湿度
        return 2.0f;
    }
    
    public float getPressure(){
        //TODO: 冲气象站获取气压
        return 3.0f;
    }
}

其他代码都不用看,我们关注measurementsChanged() 方法,通过 getXXX() 方法获取到变化之后,通知本类中的3个广告板对象调用 update(...) 方法,做出相应的更新。

IUpdate.java

public interface IUpdate {
    void update(double temp, double humidity, double presure);
}

ForecastDisplay.java

public class ForecastDisplay implements IUpdate{
    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing forecast work:"+temp+","+humidity+","+presure);
    }

}

StaticsDisplay.java

public class StaticsDisplay implements IUpdate{

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing statics work:"+temp+","+humidity+","+presure);
    }
}

CurrentConditionDisplay .java

public class CurrentConditionDisplay implements IUpdate{

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing current work:"+temp+","+humidity+","+presure);
    }
}

客户端调用:
Main.java

public class Main {

    public static void main(String[] args) {
        WeatherData wd = new WeatherData();
        wd.measurementsChanged();
    }
}

输出

I doing current work:1.0,2.0,3.0
I doing statics work:1.0,2.0,3.0
I doing forecast work:1.0,2.0,3.0

首先必须说,这样写问题真是太大了:

  1. 没有面相接口编程的思想(WeatherData类中直接使用CurrentConditionDisplay ,其实可以使用 IUpdate接口
  2. 如果还有新的广告板,需要修改WeatherData
  3. 如果通知的信号(温度,湿度,气压)再增加一个风力,那么IUpdate需要修改,所有的广告版都需要修改。

需求2:观察者模式

先上一张图:

2.png

现在我们要解决上面的3个问题:

认识观察者模式

在上面的示范中,我们其实可以看作,广告板(观察者Observer),时刻关注着 WeatherData (主题Subject) 的变化,如果有变化,则通知广告板更新。看看代码:

首先提供主题和观察者两个接口:

ISubject.java

public interface ISubject {
    void registObserver(IObserver o);
    void removeObserver(IObserver o);
    void notifyObservers();
}

IObserver .java

public interface IObserver {
    public void update(double temp, double humidity, double pressure);
}

WeatherData.java

public class WeatherData implements ISubject {
    private Set<IObserver> observers = new HashSet<IObserver>();

    double temp;
    double humidity;
    double pressure;

    public void registObserver(IObserver o) {
        observers.add(o);
    }

    public void removeObserver(IObserver o) {
        observers.remove(o);
    }

    public void notifyObservers() {
        for (IObserver o : observers) {
            o.update(temp, humidity, pressure);
        }
    }

    public void mesurementsChanged(double temp, double humidity, double pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        
        notifyObservers();
    }
}

用一个 Set<IObserver> observers 来装所有关注这个Subject的观察者,在 mesurementsChanged() 中先更新自己的数据,然后通知所有观察者并做出反应。

CurrentConditionDisplay.java

public class CurrentConditionDisplay implements IObserver{
    
    private ISubject weatherData;
    
    public CurrentConditionDisplay(ISubject weatherData){
        this.weatherData = weatherData;
        weatherData.registObserver(this);
    }

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing current work:"+temp+","+humidity+","+presure);
    }
}

ForecastDisplay.java

public class ForecastDisplay implements IObserver{
    
    private ISubject weatherData;
    
    public ForecastDisplay(ISubject weatherData){
        this.weatherData = weatherData;
        weatherData.registObserver(this);
    }

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing forecast work:"+temp+","+humidity+","+presure);
    }
}

StaticsDisplay.java

public class StaticsDisplay implements IObserver{
    
    private ISubject weatherData;
    
    public StaticsDisplay(ISubject weatherData){
        this.weatherData = weatherData;
        weatherData.registObserver(this);
    }

    public void update(double temp, double humidity, double presure) {
        System.out.println("I doing statics work:"+temp+","+humidity+","+presure);
    }
}

3个广告板类,每个包含一个要观察的主题,在初始化时添加要观察的主题,并将自己添加到主题的观察者列表中。

客户端调用:

Main.java

public class Main {

    public static void main(String[] args) {
        
        WeatherData weatherData = new WeatherData();
        
        IObserver cDisplay = new CurrentConditionDisplay(weatherData);
        IObserver sDisplay = new StaticsDisplay(weatherData);
        IObserver fDisplay = new ForecastDisplay(weatherData);
        
        weatherData.mesurementsChanged(1.0, 2.0, 3.0);
    }
}

改进的地方:

  1. 首先现在 WeatherData 可以添加任意多个观察者
  2. 主题和观察者解耦,类中保持的是接口对象。
  3. 观察主题的具体内容解耦了,WeatherData获取的信息,和Observer关心的数据分开。(简单的说就是,如果另外一种Observer之关心温度,也可是使用WeatherData,只是update方法不一样就可以了),但是也有不足指出,因为数据是由IObserver中的update方法决定的,这里感觉还可以改进。

需求3:Java中自带的观察者模式

Java 的 java.util包 中自带了 Observer接口和Observable类和我们前面的 Subject接口和Observer接口很像。

我们来看看使用Java的观察者模式,重写上面的例子:

WeatherData.java

public class WeatherData extends Observable {

    private double temperature;
    private double humidity;
    private double pressure;

    public WeatherData() {
    }

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

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

    public double getTemperature() {
        return temperature;
    }

    public double getHumidity() {
        return humidity;
    }

    public double getPressure() {
        return pressure;
    }
}

主要修改了几个地方:

  1. 继承自Observable,等下讲完,我们来看看Observable里面写了什么。
  2. setMeasurements()还是更新数据,然后调用notifyObservers()通知所有注册的Observer修改数据。所以我们应该关注 Observerable类中的两个方法:update()notifyObservers()

CurrentConditionDisplay.java

public class CurrentConditionDisplay implements Observer {

    private Observable observable;

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

    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData wd = (WeatherData) o;
            display(wd.getTemperature(), wd.getHumidity(), wd.getPressure());
        }
    }

    public void display(double temp, double humidity, double presure) {
        System.out.println("I doing current work:" + temp + "," + humidity
                + "," + presure);
    }
}

修改的地方:

  1. 与之前一样,首先还是在构造方法中添加 主题对象,并注册自己。
  2. 然后看我们重写的方法 void update(Observable o, Object arg),先判断发送update消息的主题对象是不是WeatherData,然后从主题中获取并更新数据。

思考

至于调用和之前一样,看到这里有些童鞋可能看出一掉猫腻,我们在需求2中最后提到,我们关心的消息是由 IObserver中的update 方法中的具体参数决定的,不够完善。但是这里呢?完全跟参数无关,到底从 WeatherData 中获取什么数据,可以任意获取。

我们还可以做一定的解耦,就是说 WeatherData 这个主题现在提供3种数据,但是我们完全可以将3中数据分开为3个主题,然后所有的观察者都关心自己的数据,而不需要通过WeatherData 同时获取3中数据。至于 update 中我们就需要判断 到底是 TemperatureData, HumidityData,还是 PressureData,将关心的消息也解耦了,不知道你理解没有。

源码Observable , Observer 解析

public class Observable {
    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);
    }

    
    public void notifyObservers() {
        notifyObservers(null);
    }

 
    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();
    }
}

为了节省篇幅,我狠心的把注释全删掉了,其实有很多注释的。

如果前面的内容都理解的童鞋,看这个代码也应该很好理解。套路一样,但是有一些点我们应该注意:

  1. addObserver(), deleteObserver()添加和删除观察者对象
  2. 代码中用一个 changed 关键字来表示是否有改变,在发送改变前我们需要先使用setChanged()将其设置为true。 在 notifyObservers()之后自动调用 clearChanged() 恢复 changed 为false。所以不需要我们自己来重置。
  3. 我们应该注意所有的方法都是 **synchronized **的,而且存储观察者对象的集合类也是 Vector<Observer> obs,所以整个 **Observable **都是线程安全的。

还需要看一下 Observer 接口吗?

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

确实精简,只有一个 update 方法。看看 Obsererable 对象中的 notifyObservers 方法 调用的 update:

 ((Observer)arrLocal[i]).update(this, arg);

对没错,就是这样调用的。

一些体会

感觉设计模式确实很有意思,但是上大学的时候,我们居然没有看这门课,我的天!!这个例子完整的结合了书本,自己的思考,以及Java的源码,感觉写下来我自己也受益匪浅。

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

推荐阅读更多精彩内容

  • 1 场景问题# 1.1 订阅报纸的过程## 来考虑实际生活中订阅报纸的过程,这里简单总结了一下,订阅报纸的基本流程...
    七寸知架构阅读 4,612评论 5 57
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 作者寄语 很久之前就想写一个专题,专写Android开发框架,专题的名字叫 XXX 从入门到放弃 ,沉淀了这么久,...
    戴定康阅读 7,620评论 13 85
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,918评论 25 707
  • 高中的时候以为生活就是考大学,考上好大学一切就变好了;上了大学以后发现只有找到一份靠谱的工作,才算是有个好生活;工...
    两块钱抽烟阅读 139评论 0 0