设计模式之——观察者模式

前提:本文章是以Java为基础写的

一、定义描述

什么是观察者模式?
既然我们要探讨一下观察者模式,首先还是先说一下他的定义吧;

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

现在看不懂定义很正常,在我们接下来的探讨中你就会慢慢的了解了,在我们的探讨结束之后,再回头看这个定义,你就会有所领悟了!

接下来我们就步入正题了!

二、实例探讨

例子:
现在要求我们做一个气象监测应用,这个系统大致分为三个部分,分别是气象站(获取实际气象数据的装置)、WeatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看)。
WeatherData对象知道如何跟物理气象站联系,以取得更新的数据(这个我们不需要考虑)。WeatherData对象会随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。

我们的工作:

利用WeatherData对象取得数据,并更新三个布告板:目前状况、气象统计和天气预报。

已知的一些内容如下:

WeatherData类有四个方法:getTemperature();
getHumidity();
getPressure() ;前三个方法各自返回最近的气象测量数据(温度、湿度、气压)
measurementsChanged();此方法在气象测量更新时调用
其实我们的工作就是实现measurementsChanged(),让他更新目前状况、气象统计、天气预报的显示布告板

我们现在需要解决的的问题

①实现三个使用天气数据的布告板:“目前状况”布告、“气象统计”布告、“天气预告”布告。只要WeatherData有新的测量,这些布告就必须马上更新。
②系统必须可扩展,这是什么意思呢?
就是其他开发人员可以建立定制的布告板,用户可以随心所欲地添加删除任何布告板


OK,这个题我们就分析到这里,下面就要上代码了!
代码:

public class WeatherData{
     //实例变量声明
     public void measurementChanged(){
           float temp = getTemperature();
           float humidity = getHumidity();
           float pressure = getPressure();

           currentConditionsDisplay.update(temp,humidity,pressure);//目前状况布告板的更新
           statisticsDisplay.update(temp,humidity,pressure);//气象统计布告板的更新
           forecastDisplay.update (temp,humidity,pressure);//天气预报布告板的更新
     }
 //这里是其他WaetherData方法  
}

What?

难道我们这样就解决了?so easy?好嗨哟?!
很不幸的告诉你,这个是

错误示范

接下来我们先探讨一下上面的错误示范:

先上道题瞅瞅


答案是ABCE

错误代码分析:

——现在回头看一下刚才的代码,有没有发现三个布告板都有一个update(),并且里面的形参也是一样的,那我们是不是可以把它搞成一个统一的接口呢?
——前面我们提到过,我们可能还要增加或删除布告板,那这样的话,我们岂不是要一直修改程序?


接下来我们就先来认识一下“观察者模式”这位朋友

三、认识观察者模式

我们还是通过一个例子来了解一下观察者模式吧
例子:
如果你了解报纸的订阅是怎么回事,其实就知道观察者模式是怎么回事,只是名称不太一样:出版者对应“主题”(Subject),订阅者对应“观察者”(Observer)。
其实总的来说:

出版者+订阅者=观察者模式

为了让大家看的更明白点,还是奉上图吧:



下面是鸭子、老鼠和主题的故事:(加深一下大家对观察者模式的理解)


四、定义观察者模式

观察者模式的定义我们在最开始已经说过了,经过上面我们对他的了解,现在我们再来回顾一下:

——主题和观察者定义了一对多的关系。
——观察者依赖于此主题,只要主题状态一有变化,观察者就会被通知。根据通知的风格,观察者可能因此新值而更新。
下面大家先看一下观察者模式的类图:

看了这张图你可能还会有些疑惑,不用慌,下面请欣赏我的自问自答:

问:

这和一对多的关系有啥关联啊?

答:

利用观察者模式,主题是具有状态的对象,并且可以控制这些状态。意思就是,有“一个”具有状态的主题。另外,观察者使用这些状态,虽然这些状态并不属于他们。观察者是有很多的,他们依赖主题来告诉他们状态何时改变。这就产生一个关系:

“一个”主题对“多个”观察者的关系。

问:

其中的依赖是咋产生的啊?

答:

因为主题是真正拥有数据的人,观察者是主题的依赖者,在数据变化时更新,这样比起让许多对象控制同一份数据来,可以得到更干净的00设计
不知道00设计的可以点击下面的连接:
https://www.cnblogs.com/HigginCui/p/6195318.html


说了这么多了咱们上面那个实例的问题还没有解决,不过相信大家现在对观察者模式了解的也差不多了,所以马上就到解决问题的时候了
现在呢,我们再来加深一下对观察者模式的了解:

其实观察者模式就是提供了一种对象设计,让主题和观察者之间松耦合

(这里的松耦合的意思就是:主题和观察者联系越小越好


五、解决实例的问题

首先我们还是要先捋一下我们的思路:

观察者模式是定义了一对多的依赖
——WeatherData类就是“一”,而“多"就是使用天气观测的各种布告板。

观察者模式中主题是具有状态的对象
——WeatherData对象正好是有状态的(温度、湿度、气压)。

观察者模式是主题+观察者
——我们可以把WeatherData对象当作主题,把布告板当作观察者。

——布告板获取信息,需要向WeatherData注册。

——每个布告板都有差异,我们就用一个共同的接口。
好了,差不多了,设计图就可以画出来了:


有了设计图,接下来就要干正事了!代码:

public interface Subject{
      public void registerObserver(Observer o);//注册观察者
      public void removeObserver(Observer o);//删除观察者
      public void notifyObserver();//主题改变时,这个方法会被调用,通知所有的观察者
}

public interface Observer{
      public void update(float temp, float humidity, float pressure);//观察者用于更新数据
}

public interface DisplayElement{
      public   void display();//布告板需要显示时,调用此方法
}
小探讨:

你可能会有其他想法用来实现这个问题,可能你会觉得你的方法比这个简单,代码很短,但是在你需要对这个气象应用进行扩展的时候,比如添加新的布告板,添加新的功能,你用接口的话就不需要修改很多的代码,直接写你要添加的布告板就可以了,不需要做过多的修改,有很好的封装性。
你可以自己写下试试,比较一下。


接口写好了,下面就实现我们的气象站
代码:

public class WeatherData implements Subject{
      private ArrayList observer;//我们用ArrayList数组来记录观察者
      private float temperature;
      private float humidity;
      private float pressure;

      public WeatherData(){
            observers=new ArrayList();
      }//在构造器中建立ArrayList

      public void registerObserver(Observer o){
            observers.add(o);
      }//注册观察者,直接加到ArrayList的后面就行了
      
      public void removeObserver(Observer o){
            int i = observers.indexOf(o);            if(i >= 0){
                  observers.remove(i);
            }
      }//跟注册类似,取消观察者,直接把他从ArrayList中删除

      public void notifyObservers(){
            for(int i = 0; i < observers.size(); i++){
                  Observer observer=(Observer)observers.get(i);
                  Observer.update(temprature, humidity, pressure);
            }
      }//我们把状态告诉每一个观察者,通过updata()通知他们

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

      public void setMeasurements(float temperature, float humidity, float pressure){
            this.temperature = temprature;
            this.humidity = humidity;
            this.pressure = pressure;
            measurementsChanged();
      }
//WeatherData的其他方法
}
ArrayList的相关:

https://www.cnblogs.com/rickie/articles/67978.html


上面的WeatherData类已经写好了,下面就该布告板了,三个布告板:目前状况布告板、气象统计布告板和天气预报布告板(由于三个布告板基本差不多,所以这里就只写目前状况布告板)
代码:

public class CurrentConditionsDisplay implements Observer,DisplayElement{
      private float temperature;
      private float humidity;
      private Subject weatherData;

      public CurrentConditionsDisplay(Subject weatherData){
            this.weatherData = weatherData;
            weatherData.registerObserver(this);
      }//构造器,用weatherData对象作为注册来用

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

      public void display(){
            System.out.println("Current conditions: " + temperature + "F degress and " + humidity + "% humidity");
      }//把最近的温度和湿度显示出来
}

以上代码并不是最好的,大家可以思考一下,尝试不同的方法!


接下来就剩把这些连接起来的测试程序了,Let's go!
代码:
(这里也只写了目前状况布告板)

public class WeatherStation{
      public static void main(String[] args){
            WeatherData weatherData = new WeatherData();//建立WeatherData对象
            CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);//建立布告板,传入WeatherData对象
            weatherData.setMeasurements(80,65,30.4f);
            weatherData.setMeasurements(82,70,29.2f);
            weatherData.setMeasurements(78,90,29.2f);这三组数据是我瞎写的
    }
}

请自行运行程序测试吧!
终于欧克了啊,观察者模式不过如此嘛!哈哈哈!


本来到这里是应该大结局的,可是Java实力太强,他不允许啊!

六、扩展内容

Java内置的观察者模式:

——Java API 内置的
——java.util包内有Observer接口与Observable类,这跟我们的Subject接口与Observer接口是类似
——使用贼方便,许多功Java API都给你准备好了
下面我们先了解一下java.util.Observer和java.util.Observable,下面的图是修改后的气象站00设计:


Java内置的观察者模式的运作

其实跟我们在气象站中的实现差不多,就是WeatherData(我们的主题)不太一样,现在这个是扩展自Observable类,诸如增加、删除、通知观察者的方法都继承到了,直接调用就行了。
用法:
——先介绍一下我们会用到的内置方法:
setChanged()方法:标记状态已经改变的事实;
notifyObservers()方法:数据更新时通知观察者(或者nitifyObservers(Object arg)方法;
update(Observable o,Object arg)方法:这个就是观察者更新数据的方法;

在实现代码之前,先说一下这个setChanged()方法,因为之前咱没有标记状态改变这个步骤,所以这里讲一下用它的原因。
看一下下面的代码你就懂了:

setChanged(){
      changed = true;
}

notifyObservers(Object arg){
      if(changed){
            for ever observer on the list{
                  call update(this,arg)
            }
            changed = false;//通知观察者之后,把changed标为false
      }
}//这里只会在setChanged()中的changed标为“true”时才会通知观察者

notifyObservers(){
      notifyObservers(null);
}

你可能会问:

这样做有神马必要呢?

我来告诉你:
我们用setChanged()方法可以在更新观察者是,有更多的弹性,可以更适当地通知观察者。还是打个比方发吧,如果我们不用setChanged()方法,我们的气象站测量还非常灵敏,以致于温度计读数每 1/10 就会更新,这样就会造成WeatherData对象不断地通知观察者,你想这样吗?是不是太烦人了?如果我们用setChanged()方法,我们就可以自行设置,比如我们想要在半度以上才更新,我们就可以在温度度差到达半度时,调用setChanged()方法,进行有效的更新(现在主动权就掌握在我们手中了,说让他啥时候通知就啥时候通知,真好!)


ok,接下来我们就利用Java内置的支持

重做气象站:
import java.util.Observable;
import java.util.Observer;

public class WeatherData extends Observable{   //看好啊,我们现在正在**继承**Observable
      private float temperature;
      private float humidity;
      private float pressure;

      public WeatherData(){ }//构造器不需要为了记住观察者而建立ArrayList了

      public void measurementsChanged(){
            setChanged();//这里,在调用notifyObservers()之前,先调用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;
      }
//用上面这三个方法取得WeatherData对象的状态
}
重做目前状况布告板:
import java.util.Observable;
import java.util.Observer;

public class CurrentConditionsDisplay implements Observer, DisplayElement{  //我们这里正在实现java.util.Observer接口
      Observable observable;
      private float temperature;
      private float humidity;

      public CurrentConditionsDisplay(Observable observable){ //构造器需要Observable当参数,并将CurrentConditionsDisplay对象登记成为观察者
            this.observable = observable;
            observable.addObserver(this);
      }
      
      public void update(Observable obs, Object arg){  //改变update()方法,增加Observable和数据对象作为参数
            if(obs instanceof WeatherData){   //这里的instanceof的作用是:确定可观察者(主题)属于WeatherData类型
                  WeatherData weatherData = (WeatherData)obs;
                  this.temperature = weatherData.getTemperature();
                  this.humidity = weatherData.getHumidity();
                  display();
            }
      }

      public void display(){
            System.out.println("Current conditions: " + temperature + "F degress and " + humidity + "% humidity");
      }
}
这里是对instanceof的介绍:

https://www.cnblogs.com/zjxynq/p/5882756.html
来看一下运行结果吧:


这是完整版的运行结果,你们记得把另外两个布告板也写上去!

通过看这个运行结果,我们能看出啥呢?

emmm...他这个输出次序好像有点乱啊,本来的次序是:目前状况—>气象统计—>天气预报;

那现在咋成这样了呢?思考一下,我们继续

不要依赖于观察者被通知的次序

java.util.Observable实现了他的notifyObservers()方法,这导致了通知者的次序不同于我们先前的次序。谁都没错,只是双方选择不同的方式罢了。

但是,我们的依赖这样的次序来写,就是错的!这样一来,只要观察者的实现有所变化,通知次序就会改变,很有可能就会产生错误的结果。所以这就不可能是我们所要的松耦合(违背了初心啊!)

下面就来看一下原因吧!


现在应该明白了吧?
我在前面的代码的注释中就特别说明了Observable是,而非是我们想要的接口

除了这个内置的观察者模式,下面还有一些扩展,继续看下去吧
Come on!!!

观察者与Swing

其实观察者模式还是比较厉害的,不仅是在java.util中可以见到,而且在JavaBeansSwing中,也都有观察者模式。
——JavaBeans中的观察者模式的话,可以直接查一下PropertyChangeListener接口。
——Swing中的,我们就用例子来说一下:

一个小的、改变生活的程序

程序非常简单,有一个按钮,上面写着“Should I do it ?”(我该做吗?)。当你按下按钮,倾听者(观察者)就回答问题。我们这里就实现两个倾听者吧,一个天使(AngelListener),一个恶魔(DevilListener)。
提示:我们这里用的是Swing API:JButton
这个程序代码很短,就建立一个JButton对象,把他加到JFrame,然后设置好倾听者(观察者)就可以了。我们这里打算用内部类作为倾听者。
代码:

public class SwingObserverExample{
      JFrame frame;
      
      public static void main(String[] args){
            SwingObserverExample example = new SwingObserverExample();
            example.go();
      }
      public void go(){
            frame = new JFrame();
            JButton button = new JButton("Should I do it ?");
            button.addActionListener(new AngelListener());
            button.addActionListener(new DevilListener());
            frame.getContentPane().add(BorderLayout.CENTER, button);//在这里设置frame的属性
      }

      class AngelListener implements ActionListener{
            public void actionPerformed(ActionEvent event){
                  System.out.println("Don't do it, you might regret it !");
            }
      }//天使倾听者(观察者)

      class DevilListener implements ActionListener{
            public void actionPreformed("Come on, do it !");
            }//这里的actionPerformed()对应之前的update()
      }//恶魔倾听者(观察者)
}

最后一个板块:

七、总结

工具

——00原则:

  • 封装变化;
  • 多用组合,少用继承;
  • 针对接口编程,不针对实现编程;
  • 为交互对象之间的松耦合设计而努力。
要点
  • 观察者模式定义了对象之间一对多的关系;

  • 可观察者(主题)用一个共同的接口来更新观察者;

  • 观察者和可观察者(主题)之间用松耦合方式结合,可观察者不知道观察者的细节,只知道观察者实现了观察者接口;

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

  • Java有多种观察者模式的实现,包括了通用的java.util.Observable;

  • 可以尝试实现自己的Observable;

  • Swing大量使用观察者模式,许多GUI框架也是如此;

  • 此模式也被应用在很多地方,例如:JavaBeansRMI
    这次的观察者模式算是真正的讲完了,写的不咋样,大家凑合着看吧,有不懂的可以评论或者私聊!
    关于代码中的命名问题,这里推荐大家看一下《阿里Java开发手册》
    (提取码:aviz)

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

推荐阅读更多精彩内容