有趣的事情发生时,可千万别错过了!
观察者模式是JDK中使用最多的模式之一,非常有用。我们也会一并介绍一对多关系,以及松耦合。有个观察者,你将会消息灵通。
工作合约
某软件公司接到一个工作合约,内容如下:
恭喜贵公司获选为敝公司(Weather-O-Rama气象站)建立下一代Internate气象站!
该气象站必须建立在我们专利申请中的WeatherData对象上,由WeatherData负责监测目前的天气情况(温度、湿度和气压等)。我们希望贵公司建立一个应用,有三种布告板,分别显示目前的情况,气象统计及简单的预报。当WeatherData对象获得最新的监测数据时,三种布告板实时更新。
而且,这是一个可以拓展的气象站,Weather-O-Rama气象站希望发布一组API,好让其他开发人员可以写出自己的气象布告板,并插入次应用中。我们希望贵公司提供这样的API。
气象监测站应用的概况
此系统中的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(最终来自气象站的数据)和布告板(显示目前天气状况给用户看)。如图:
WeatherData对象知道如何跟物理气象站联系,以获得监控的实施气象数据。WeatherData对象会随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。
我们的工作就是建立一个应用,利用WeatherData对象获得数据,并更新三个布告板。
瞧一瞧客户给提供的WeatherData类吧。
我们的工作是实现measurementsChanged()方法,好让它更新目前的状况、气象统计和天气预报的显示布告板。
我们目前知道什么?
Weather-O-Rama气象站的要求说明并不是很清楚,我们必须搞懂该做些什么,那么,我们目前知道些什么呢?
WeatherData类具有getter方法,可以取得三个测量值:温度、湿度与气压。
当新的测量数据准备好时,measurementsChanged()方法就会被调用(我们不在乎次方法是如何被调用的,我们只在乎它被调用了,也许被某个其他的程序调用的,比如硬件采集器,这是Weather-O-Rama气象站提供的WeatherData类已经实现的功能)。
我们需要实现三个使用天气数据的布告板,一旦WeatherData有新的测量数据就马上更新。
次系统必须可拓展,让其他开发人员自定义布告板。
先看一个错误的示范
如果不进行思考,直接进入开发阶段,只为了完成工作而工作,那么这是第一个可能的实现。我们依照Weather-O-Rama气象站开发人员的暗示,在客户提供的类的measurementsChanged()方法中添加我们的代码:
package cn.net.bysoft.observer; /** * 这是一个没有经过任何思考而写的代码。 * */ public class WeatherData { // 该方法在硬件有最新监测数据时被调用。 public void measurementsChanged() { // 该类中的代码又我们公司实现。 // 调用三个getter方法获得最新的监测数据。 float temp = getTemperature(); float humidity = getHumidity(); float pressure = getPressure(); // 更新三个我们公司自己开发的布告板类。 currentConditionsDispaly.update(temp, humidity, pressure); statisticsDisplay.update(temp, humidity, pressure); forecastDispaly.update(temp, humidity, pressure); } public float getTemperature() { return temperature; } public float getHumidity() { return humidity; } public float getPressure() { return pressure; } // 这三个setter方法可能会在硬件监测到最新的气象数据时被赋值。 // 然后硬件会调用measurementsChanged()方法。 public void setTemperature(float temperature) { this.temperature = temperature; } public void setHumidity(float humidity) { this.humidity = humidity; } public void setPressure(float pressure) { this.pressure = pressure; } private float temperature; private float humidity; private float pressure; }
在我们的第一个实现中,存在一下的缺点:
我们是针对具体实现编程,而非针对接口;
对于每个新的布告版,我们都得修改代码;
我们无法在运行时动态地添加或删除布告板;
我们尚未封装改变的部分;
我们的实现有什么不对?
针对具体实现编程,会导致我们以后在添加或删除布告板时必须修改程序。而且,布告板的update()方法看起来是一个统一的接口,布告板的方法名称都是update(),参数都是温度、湿度和气压。
现在就来看看观察者模式,然后再回来看看如何将此模式应用到气象观测站。
观察者就好像订阅报纸或者杂志。
报社的业务就是出版报纸。
向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸。
当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
只要报社还在运营,就会一直有人向他们订阅报纸或取消订阅。
订阅者+出版者=观察者模式,只是名称不太一样。
出版者改为主题(Subject),订阅者改为观察者(Observer),一个主题对应多个观察者,是一个一对多的关系。来看一下观察者的类图:
这里有一个设计原则:
设计原则:
为了交互对象之间的松耦合设计而努力。
松耦合的威力
当两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此的细节。
观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
关于观察者的一切,主题值知道观察者实现了某个接口。主题不需要知道观察者的具体类是谁,做了些什么或其他任何细节。
任何时候我们都可以添加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时添加观察者。事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何时候删除某些观察者。
有新类型出现的观察者出现时,主题的代码不需要修改。我们可以独立地复用主题或观察者。如果我们在其他任何地方需要使用主题或观察者,可以轻易地复用,因为二者并非紧耦合,而是松耦合。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
设计与实现气象站
了解了这么多,开始设计与实现气象站吧。
设计类图如上图所示,让我们从建立接口开始吧!
package cn.net.bysoft.observer; /** * 布告板显示信息接口。 * */ public interface DisplayElement { /** * 当布告板需要显示时,调用次方法。 * */ public void display(); }
package cn.net.bysoft.observer; /** * 观察者接口。 * */ public interface Observer { /** * 当气象监测数据改变时,主题会把这些状态值当作方法的参数,传送给观察者。 * */ public void update(float temp, float humidity, float perssure); }
package cn.net.bysoft.observer; /** * 主题接口。 * */ public interface Subject { /** * registerObserver和removeObserver方法都需要一个观察者作为变量,该观察者是用来注册或被删除的。 * */ public void registerObserver(Observer ob); public void removeObserver(Observer ob); /** * 当主题的状态改变时,这个方法会被调用,已通知所有的观察者。 * */ public void notifyObservers(); }
接下来,我们要用观察者模式实现WeatherData类了。
package cn.net.bysoft.observer; import java.util.ArrayList; public class WeatherData implements Subject { public WeatherData() { observers = new ArrayList<Observer>(); } /** * 注册观察者。 * */ public void registerObserver(Observer ob) { observers.add(ob); } /** * 移除观察者。 * */ public void removeObserver(Observer ob) { int index = observers.indexOf(ob); if (index >= 0) observers.remove(index); } /** * 当有最新的气象监测数据时被调用。↓下面的函数调用。 * */ public void notifyObservers() { for (Observer ob : observers) { ob.update(this.temperature, this.himidity, this.pressure); } } /** * 气象站硬件监测到新数据会调用该方法。↓下面的函数调用。 * */ public void measurementsChanged() { notifyObservers(); } /** * 因为没有气象站,所以模拟一个硬件,获得气象数据调用measurementsChanged()方法。 * 在main()函数中使用。 * */ public void setMeasurements(float temperature, float himidity, float pressure) { this.temperature = temperature; this.himidity = himidity; this.pressure = pressure; measurementsChanged(); } public ArrayList<Observer> getObservers() { return observers; } public void setObservers(ArrayList<Observer> observers) { this.observers = observers; } public float getTemperature() { return temperature; } public void setTemperature(float temperature) { this.temperature = temperature; } public float getHimidity() { return himidity; } public void setHimidity(float himidity) { this.himidity = himidity; } public float getPressure() { return pressure; } public void setPressure(float pressure) { this.pressure = pressure; } private ArrayList<Observer> observers; private float temperature; private float himidity; private float pressure; }
最后,编写布告板吧!
我们已经把WeatherData类写出来了,现在轮到布告板了。Weather-O-Rama气象站订购了三个布告板,首先来实现其中一个布告板。
package cn.net.bysoft.observer; public class CurrentConditionsDisplay implements Observer, DisplayElement { public CurrentConditionsDisplay() {} public CurrentConditionsDisplay(Subject weatherData) { // 实例化布告板的时候,传入一个主题,将自己注册到主题中。 this.weatherData = weatherData; weatherData.registerObserver(this); } public void display() { System.out.println("当前conditions:" + temperature + "F degrees and " + himidity + "% humidity"); } public void update(float temp, float humidity, float perssure) { this.temperature = temp; this.himidity = humidity; // 更新数据后刷新布告板。 display(); } private float temperature; private float himidity; // 要观察的主题。 private Subject weatherData; }
布告板已经实现完毕,编写一个main()方法测试一下观察者模式吧!
package cn.net.bysoft.observer; public class Main { public static void main(String[] args) { // 首先,创建一个主题。 // 这里应该使用Subject weatherData = new WeatherData();来创建主题。 // 但是我们在WeatherData中添加了一个模拟硬件发送气象数据的函数,Subject中并没有这个函数。 // 所以使用了WeatherData来创建主题。 WeatherData weatherData = new WeatherData(); // 建立一个观察者(也就是布告板),把主题传给观察者,在观察者的构造函数中订阅主题。 Observer ccDisplay = new CurrentConditionsDisplay(weatherData); // 模拟一个气象变化的情况。 weatherData.setMeasurements(80, 65, 30.4f); System.out.println("\n====================\n"); // 又变化了! weatherData.setMeasurements(80, 65, 30.4f); /** * output: * 当前conditions:80.0F degrees and 65.0% humidity * * ==================== * * 当前conditions:80.0F degrees and 65.0% humidity * * */ } }
另外两个布告板如上代码,另外,Java中提供了内置的观察者模式接口,它们是:
java.util.Observable
java.util.Observer
想要进一步了解可以网上查找资料,或者和我一样购买HeadFirst设计模式这本书,这本书真的很不错。
以上就是观察者模式。