设计模式之——观察者模式
前提:本文章是以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中可以见到,而且在JavaBeans和Swing中,也都有观察者模式。
——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框架也是如此;
-
此模式也被应用在很多地方,例如:JavaBeans、RMI
这次的观察者模式算是真正的讲完了,写的不咋样,大家凑合着看吧,有不懂的可以评论或者私聊!
关于代码中的命名问题,这里推荐大家看一下《阿里Java开发手册》
(提取码:aviz)*本文图片例子来源于《Head First 设计模式》*