headFirst设计模式——观察者模式
一、介绍
观察者(Observer)模式可以帮你的对象知悉现况,不会错过该对象感兴趣的事。
对象甚至在运行时可决定是否要继续被通知。以及一对多关系,和松耦合。
有了观察者,你将会消息灵通。
观察者模式定义了对象之间的一对多依赖,这样一来当一个对象改变状态时,
它的所有依赖着都会收到通知并自动更新。
实现观察者模式的方法不止一种,但是以包含Subject和Observer接口的类设计的做法最常见。
二、引入
恭喜贵公司获选为敝公司建立下一代Internet气象观测站!
该气象站必须建立在我们专利申请中的WeatherData对象
上,由WeatherData对象负责追踪目前的天气状况(温度、
湿度、气压)。我们希望贵公司能建立一个应用,有三种
布告板,分别显示目前的状况、气象统计及简单的预报。
当WeatherObject对象获得最新的测量数据时,三种布告板
必须实时更新。
而且,这是一个可以扩展的气象站,Weather-O-Rama气象
站希望公布一组API,好让其他开发人员可以写出自己的
气象布告板,并插入此应用中。我们希望贵公司能提供这
样的API。
Weather-O-Rama气象站有很好的商业营运模式:一旦客
户上钩,他们使用每个布告板都要付钱。最好的部分就是,
为了感谢贵公司建立此系统,我们将以公司的认股权支付
你。
我们期待看到你的设计和应用的alpha版本。
WeatherData对象知道如何跟物理气象站联系,以取得更新的数据。WeatherData对象随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。
我们的工作就是建立一个应用,利用WeatherData对象取得数据,并更新三个布告板:目前状况、气象统计和天气预报。
报纸的订阅 出版者+订阅者=观察者模式
出版者改称为“主题”(Subject),订阅者改称为“观察者”(Observe)。
设计原则:
1、为了交互对象之间的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
三、实现
Java为观察者模式提供了内置支持,但是许多时候,自己建立这一切会更具弹性。
主题
1 public interface Subject { 2 3 public void registerObserver(Observer o); 4 5 public void removeObserver(Observer o); 6 7 public void notifyObservers(); 8 }
主题实现类
1 public class WeatherData implements Subject{ 2 private ArrayList<Observer> observers; 3 private float temperature; 4 private float humidity; 5 private float pressure; 6 7 // 我们加上一个ArrayList来纪录观察者,此ArrayList是在构造器中建立的。 8 public WeatherData(){ 9 observers = new ArrayList<Observer>(); 10 } 11 12 @Override 13 public void registerObserver(Observer o) { 14 observers.add(o); 15 } 16 17 @Override 18 public void removeObserver(Observer o) { 19 int i = observers.indexOf(o); 20 if (i >= 0){ 21 observers.remove(i); 22 } 23 } 24 25 @Override 26 public void notifyObservers() { 27 // 在这里,我们把状态告诉每一个观察者。因为观察者都实现了update(),所以我们知道如何通知它们。 28 for (int i = 0; i < observers.size(); i++) { 29 Observer observer = observers.get(i); 30 observer.update(temperature, humidity, pressure); 31 } 32 } 33 34 public void measurementsChanged(){ 35 // 当从气象站得到更新观测值 时,我们通知观察者。 36 notifyObservers(); 37 } 38 39 public void setMeasurements(float temperature, float humidity, float pressure) {//模拟从气象站收到数据 40 this.temperature = temperature; 41 this.humidity = humidity; 42 this.pressure = pressure; 43 measurementsChanged(); 44 } 45 46 }
观察者
1 public interface Observer { 2 3 // 暗示:这些观测值的种类和个数在未来有可能改变吗?如果以后会改变,这些变化是否被很好地封装?或者是需要修改许多代码才能办到? 4 public void update(float temp, float humidity, float pressure); 5 }
每个观察者必须实现的接口
1 public interface DisplayElement { 2 3 public void display(); 4 }
观察者实现类
1 public class CurrentConditionsDisplay implements DisplayElement, Observer { 2 private float temperature; 3 private float humidity; 4 private Subject weatherData; 5 6 // 构造器需要 weatherData对象(也就是主题)作为注册用。 7 public CurrentConditionsDisplay(Subject weatherData) { 8 this.weatherData = weatherData; 9 weatherData.registerObserver(this); 10 } 11 12 @Override 13 public void update(float temperature, float humidity, float pressure) { 14 this.temperature = temperature; 15 this.humidity = humidity; 16 display(); // 这里并不是调用display()最合适的地方。 17 } 18 19 @Override 20 public void display() { 21 System.out.println("Current conditions: "+ temperature + 22 "F degrees and " + humidity +"%humidity"); 23 } 24 25 }
测试
1 public class WeatherStation { 2 3 public static void main(String[] args) { 4 // 主题 5 WeatherData weatherData = new WeatherData(); 6 // 订阅者 向 主题 注册 7 CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); // Subject引用可能在取消订阅时用到。 8 9 // 模拟从气象站取得数据 10 weatherData.setMeasurements(80, 65, 30.4f); // Current conditions: 80.0F degrees and 65.0%humidity 11 } 12 }
四、变化
JohnnyAHurricane(Weather-O-Rama气象站的CEO)刚刚来电告知,他们还需要酷热指数(HeatIndex)布告板,这是不可或缺的。
细节如下:
酷热指数是一个结合温度和湿度的指数,用来显示人的温度感受。可以利用温度T和相对湿度RH套用下面的公式来计算酷热指数:
一个关于温度和相对湿度的计算公式。
新注册一个观察者HeadIndexDisplay
1 public class HeatIndexDisplay implements Observer, DisplayElement { 2 private float tempreture; 3 private float humidity; 4 private Subject weatherData; 5 6 public HeatIndexDisplay(Subject weatherData) { 7 this.weatherData = weatherData; 8 weatherData.registerObserver(this); 9 } 10 11 @Override 12 public void display() { 13 float T = tempreture; 14 float RH = humidity; 15 // 公式不对 16 float heatIndex = 17 (float) (16.923 + 1.85212 * 10-1 * T + 5.37941 * RH - 1.00254 * 10-1 * T 18 * RH + 9.41695 * 10-3 * T*T + 7.28898 * 10-3 * RH*RH + 3.45372 * 10-4 19 * T*T * RH - 8.14971 * 10-4 * T * RH*RH + 1.02102 * 10-5 * T*T * RH*RH - 20 3.8646 * 10-5 * T*T*T + 2.91583 * 10-5 * RH*RH*RH + 1.42721 * 10-6 * T*T*T * RH 21 + 1.97483 * 10-7 * T * RH*RH*RH - 2.18429 * 10-8 * T*T*T * RH*RH + 8.43296 * 22 10-10 * T*T * RH*RH*RH - 4.81975 * 10-11 * T*T*T * RH*RH*RH); 23 24 System.out.println("HeadIndex is" + heatIndex); 25 } 26 27 @Override 28 public void update(float tempreture, float humidity, float pressure) { 29 // TODO Auto-generated method stub 30 this.tempreture = tempreture; 31 this.humidity = humidity; 32 display(); 33 34 } 35 36 }
五、讨论
主题和观察者就 使观察者获得状态信息的正确方式:
1、主题主动推送数据
方便不需要再去拉数据,但是有时候不需要的也会推送过来。
2、观察者主动取数据
用时去取,主题需要封装、再开放setter避免暴露过多信息。
java内置Observer这两种都支持。
java.util包(package)内包含最基本的Observer接口与Observable类,这和Subject接口与Observer接口很相似。
Observer接口与Observable类使用上更方便,因为许多功能都已经事先准备好了。
甚至可以使用推(push)或拉(pull)的方式传送数据。
setChanged()方法的作用:
setChanged()方法可以让你在更新观察者时,有更多的弹性,你可以更适当地通知观察者。
比方说,如果没有setChanged()方法,我们的气象站测量是如此敏锐,以致于温度计读数每十分之一度就会更新,
这会造成WeatherData对象持续不断地通知观察者,我们并不希望看到这样的事情发生。
如果我们希望半度以上才更新,就可以在温度差距到达半度时,调用setChanged(),进行有效的更新。
主题
1 public class WeatherData extends Observable{ 2 3 private float temperature; 4 private float humidity; 5 private float pressure; 6 7 // "拉"察者会利用这些方法取得WeatherData对象的状态 8 public float getTemperature() { 9 return temperature; 10 } 11 12 public float getHumidity() { 13 return humidity; 14 } 15 16 public float getPressure() { 17 return pressure; 18 } 19 20 public void setMeasurements(float temperature, float humidity, float pressure) { 21 this.temperature = temperature; 22 this.humidity = humidity; 23 this.pressure = pressure; 24 measurementsChanged(); 25 } 26 27 public void measurementsChanged(){ 28 setChanged(); 29 notifyObservers(); //注意:我们没有调用notifyObservers()传送数据对象,这表示我们采用的做法是“拉”。 30 } 31 }
观察者
1 import java.util.Observable; 2 import java.util.Observer; 3 4 public class CurrentConditionsDisplay implements Observer,DisplayElement { 5 6 private float temperature; 7 private float humidity; 8 Observable o; 9 10 public CurrentConditionsDisplay(Observable o) { 11 this.o = o; 12 o.addObserver(this); 13 } 14 15 @Override 16 public void update(Observable obs, Object arg) { 17 if(obs instanceof WeatherData){ 18 WeatherData weatherData = (WeatherData) obs; 19 this.temperature = weatherData.getTemperature(); 20 this.humidity = weatherData.getHumidity(); 21 display(); 22 } 23 } 24 @Override 25 public void display() { 26 System.out.println("Current conditions: "+ temperature + 27 "F degrees and " + humidity +"%humidity"); 28 } 29 30 }
内置观察者模式的问题:
1、通知观察者的次序不一定是其注册的次序。
可以确定的是,如果我们的代码依赖之前的次序,就是错的。
一旦观察者/可观察者的实现有所改变,通知次序就会改变,很可能产生错误的结果,也不是我们所认为的松耦合。
2、java.util.Observable是一个类而不是一个接口
如果某类想同时具有Observable类和另一个超类的行为,就会陷入两难,毕竟Java不支持多重继承。
这限制了Observable的复用潜力(而增加复用潜力不正是我们使用模式最原始的动机吗?)。
再者,因为没有Observable接口,所以你无法建立自己的实现,和Java内置的Observer API搭配使用。
3、Observable将关键的方法保护起来
如果你看看Observable API,你会发现setChanged()方法被保护起来了(被定义成protected)。
这意味着:除非你继承自Observable,否则你无法创建Observable实例并组合到你自己的对象中来。
这个设计违反了第二个设计原则:“多用组合,少用继承”。
六、总结
观察者模式定义了对象之间一对多的关系。
主题(也就是可观察者)用一个共同的接口来更新观察者。
观察者和可观察者之间用松耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。
使用此模式时,你可从被观察者处推(push)或拉(pull)数据(然而,推得方式被认为更“正确”)。
有多个观察者时,不可以依赖特定的通知顺序。
Java有多种观察者模式的实现,包括了通用的java.util.Observable。
要注意java.util.Observable所带来的的一些问题。
如果有必要的话,可以实现自己的Observable。
设计原则:
1、封装变化
2、多用组合,少用继承
3、针对接口编程,不针对实现编程
4、为交互之间的松耦合设计儿努力 // 新原则
『观察者模式』Obverse Pattern 在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的其他对象都会收到通知,并自动更新。
一个新的模式,以松耦合方式在一系列对象之间沟通状态。我们目前还没看到观察者模式的代表人物——MVC。