观察者模式:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
知识点的梳理:
- 为了交互对象之间的松耦合设计而努力;
- 主题(也就是可观察者)用一个共同的接口来更新观察者;
- 观察者和可观察者之间用松耦合方式结合,可观察者不知道观察者的细节,只知道观察者实现了观察者接口;
- 有多个观察者时,不可以依赖特定的通知次序;
-
Swing大量使用观察者模式,许多GUI框架也是如此;
- 此模式也被应用到许多地方,如JavaBean,RMI;
-
气象监测应用的概况
-
此系统中的三个部分是气象站(获取实际气象数据的物理装置),WeatherData对象(追踪自来气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看);
-
- WeatherData对象知道如何跟物理气象站联系,以取得更新的数据。WeatherData对象会随即更新三个布告板的显示:目前状况(温度,湿度,气压),气象统计和天气预报;
- 我们需要建立一个应用,利用WeatherData对象取得数据,并更新三个布告板:目前状况,气象统计和天气预报;
-
-
我们现在知道什么?
- WeatherData类具有getter方法,可以取得三个测量值:温度,湿度与气压:
-
|
getTemperature(); |
- 当新的测量数据备妥时,measurementsChanged()方法就会被调用:
|
measurementsChanged() |
- 需要实现三个使用天气数据的布告板:"目前状况"布告,"气象统计"布告,"天气预报"布告。一旦WeatherData有新的测量,这些布告必须马上更新;
- 此系统可扩展,让其他开发人员建立定制的布告板,用户可以随心所欲地添加或删除任何布告板。
-
错误示范
- 在measurementsChanged()方法中添加代码:
|
public class WeatherData { public void measurementsChanged(){ //调用WeatherData的三个getXxx()方法,以取得最近的测量值。这些getXxx()方法已经实现好了 float temp = getTemperature(); float humidity = getHumidity(); float pressure = getPressure(); //更新布告板 currentConditionsDisplay.update(temp,humidity,pressure); statisticsDisplay.update(temp,humidity,pressure); forecastDisplay.update(temp,humidity,pressure); } //其他WeatherData方法 } |
-
实现有什么不对?
-
-
什么是观察者模式
-
报纸和杂志的订阅是怎么回事?
- 报社的业务就是出版报纸;
- 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸;
- 当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸;
- 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸;
-
出版者+订阅者=观察者模式
-
结合报纸的订阅:出版者改称为"主题"(subject),订阅者改称为"观察者"(Observer);
-
-
-
图解观察者模式
-
-
定义观察者模式:类图
-
-
-
松耦合的威力
- 两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节;
- 对于观察者,主题只知道观察者实现了某个接口(也就是Observer接口),主题不需要知道观察者的具体类是谁,做了些什么或其他任何细节;
- 任何时候都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以可以随时增加观察者。实际上,在运行的时候,可以用新的观察者取代现有的观察者,主题不会受到任何影响。同理,也可以在任何时候删除这些观察者;
-
有新类型的观察者出现时,主题的代码不需要修改。
- 加入我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可;
- 主题不在乎别的,它只会发送通知给所有实现了观察者接口的对象;
- 可以独立地复用主题或者观察者。如果我们在其他地方需要使用主题或观察者,可以轻易地复用,因为二者并非紧耦合;
- 改变主题或者观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间的接口仍被遵守,就可以自由的改变它们;
-
设计气象站
- WeatherData类符合观察者模式中,所说的"一",而"多"正是使用天气观测的各种布告板;
-
如何将气象观测值放到布告板上呢?
- 如果将WeatherData对象当作主题,把布告板当作观察者,布告板为了取得信息,就必须先向WeatherData对象注册;
- 一旦WeatherData知道有某个布告板的存在,就会适时地调用布告板的某个方法来告诉布告板观测值是多少;
- 每个布告板都有差异,需要一个共同的接口。虽然布告板的类不一样,但它们应该实现相同的jie'k
- 所以,每个布告板都应该有一个名为update()的方法,以供WeatherData对象调用;而这个方法应该在所有布告板都实现的共同接口里定义;
-
设计图
-
-
实现气象站
- 先建立一些接口
|
public interface Subject { //这两个方法都需要一个观察者作为变量,该观察者是用来注册或被删除的 void registerObserver(Observer o); void removeObserver(Observer o); //当主题状态改变时,这个方法会被调用,以通知所有的观察者 void notifyObservers(); } |
|
public interface Observer { /** * 所有观察者都必须实现此方法,以实现观察者接口。把观测值传入观察者 * 当气象观测值改变时,主题会把这些状态值当作方法的参数,传送给观察者 */ void update(float temp,float humidity,float pressure); } |
|
/* * 该接口只包含一个方法。当布告板需要显示时,调用此方法。 */ public interface DisplayElement { public void display(); } |
- 在WeatherData中实现主题接口
|
public class WeatherData implements Subject { //此字段用来记录观察者,在构造函数中建立 private ArrayList observers; private float temperature; private float humidity; private float pressure; public WeatherData(){ observers = new ArrayList(); } //当从气象站得到更新观测值时,我们通知观察者 public void measurementsChanged(){ notifyObservers(); }
//当注册观察者时,只要把它加到ArrayList的后面即可 @Override public void registerObserver(Observer o) { observers.add(o); }
//当观察者想取消注册,我们把它从ArrayList中删除即可 @Override public void removeObserver(Observer o) { int i = observers.indexOf(o); if(i >= 0 ){ observers.remove(i); } }
//把状态告诉每一个观察者。因为观察者都实现了update(),所以可以通知它们 @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 setMeasurements(float temperature,float humididty,float pressure){ this.temperature = temperature; this.humidity = humididty; this.pressure = pressure; measurementsChanged(); } //其他WeatherData方法 } |
-
建立布告板
- 需要三个布告板:目前状况布告板,统计布告板,预测布告板。先来实现目前状况布告板
|
/** * 实现了Observer接口,所以可以从WeatherData对象中获得改变 * 实现了DisplayElement接口,因为我们的API规定所有的布告板都必须实现此接口 */ public class CurrentConditionsDisplay implements Observer, DisplayElement { private float temperature; private float humidity; private Subject weatherData; //构造器需要weatherData对象(也就是主题)作为注册使用 public CurrentConditionsDisplay(Subject weatherData){ this.weatherData = weatherData; weatherData.registerObserver(this); } //该方法只是显示最近的温度和湿度 @Override public void display() { System.out.println("Current conditions:"+temperature+"F degrees and"+humidity+"% humidity"); }
@Override public void update(float temp, float humidity, float pressure) { //当update()被调用时,我们把温度和湿度保存起来,然后调用display() this.temperature =temperature; this.humidity = humidity; display(); } } |
-
启动气象站
- 先建立一个测试程序
|
public class WeatherStation { public static void main(String[] args) { //首先,建立一个WeatherData对象 WeatherData weatherData = new WeatherData(); CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); weatherData.setMeasurements(80, 65, 30.4f); weatherData.setMeasurements(82, 70, 29.2f); weatherData.setMeasurements(78, 90, 29.2f); } } |
- 效果:
|
|
-
使用Java内置的观察者模式
-
java.util包(package)内包含最基本的Observer接口与Observable类,这和Subject接口与Observer接口很相似;
- 有了Java内置的支持,只需要扩展(继承)Observable,并告诉它何时该通知观察者就可以了;
-
java.util.Observer和java.util.Observable的类结构
-
-
如何把对象变成观察者?
- 实现观察者接口(java.util.Observer),然后调用任何Observable对象的addObserver()方法。不想再当观察者,调用deleteObserver()方法即可;
-
观察者如何送出通知?
-
需要利用扩展java.util.Observable接口产生"可观察者"类,然后,需要两个步骤:
- 先调用setChanged()方法,标记状态已经改变的事实;
-
然后调用两种notifyObservers()方法中的一个:
- notifyObservers();
- notifyObservers(Object arg);//当通知时,此版本可以传送任何的数据对象给每一个观察者
-
-
观察者如何接受通知?
-
同以前一样,观察者实现更新方法,但签名不一样:
-
- 推送数据给观察者,可以把数据当作数据对象传送给ntifyObservers(args)方法。否则,观察者就必须从可观察者对象中"拉"(pull)数据。
-
-
利用内置的支持重做气象站
- 首先,把WeatherData改成使用java.util.Observable
-
|
package hey.up2; //导入Java内置观察者package import java.util.Observable; import java.util.Observer;
//现在继承Observable public class WeatherData extends Observable { private float temperature; private float humidity; private float pressure; public WeatherData(){ //不再需要构造函数来记住观察者们了 } //当从气象站得到更新观测值时,我们通知观察者 public void measurementsChanged(){ //在调用notifyObservers函数之前,要先调用serChanged()来指示状态已经改变 setChanged(); notifyObservers(); } //利用这个方法来测试布告板 public void setMeasurements(float temperature,float humididty,float pressure){ this.temperature = temperature; this.humidity = humididty; this.pressure = pressure; measurementsChanged(); } //观察者需要这些getter方法,取得WeatherData对象的状态 public float getTemperature() { return temperature; } public float getHumidity() { return humidity; } public float getPressure() { return pressure; } } |
- 现在重做CurrentConditionsDisplay
|
package hey.up2; import java.util.Observable; import java.util.Observer; /** * 实现java.util.Observer接口 */ public class CurrentConditionsDisplay implements Observer, DisplayElement { Observable observable; private float temperature; private float humidity; //现在构造函数需要一个Observable当参数,并将CurrentCondi-tionsDisplay对象登记成为观察者 public CurrentConditionsDisplay(Observable observable){ this.observable = observable; observable.addObserver(this); }
//改变update方法,增加Observable和数据对象作为参数 @Override public void update(Observable obs, Object arg) { //此方法先确定可观察者属于WeatherData类型,然后利用getter方法获取温度和湿度测试值,最后调用display() if(obs instanceof WeatherData){ WeatherData weatherData = (WeatherData)obs; this.temperature = weatherData.getTemperature(); this.humidity = weatherData.getHumidity(); display(); } } //该方法只是显示最近的温度和湿度 @Override public void display() { System.out.println("Current conditions:"+temperature+"F degrees and"+humidity+"% humidity"); } } |
- 继续使用WeatherStation进行测试,效果如下:其实是一样的!
|
|
-
java.util.Observable的缺点
- 它一个类,而非接口;
- Observable的setChanged()方法的访问修饰符为protected。除非我们继承自Observable,否则无法创建Observable实例,并组合到自己的对象中。这违反了第二个设计原则:"多用组合,少用继承";