学习Head First Design Pattern——翻译Chapter 2:The Observer Pattern
Page 37
图片〉〉 嗨Jerry,模式组讨论已经推迟到星期六的晚上了,我正要通知大家呢。这次我们将讨论OBSERVER模式,这个模式真的很棒,真的,Jerry! |
不要错过有趣的事情!有个模式,它能让你的对象都能够及时了解到它们所关心的正在发生的事情,甚至,对象可以在运行时决定是否被通知。OBSERVER是在JDK中使用最频繁的模式,它的作用很大!在我们开始学习该模式的之前,我们首先了解两个概念:一对多关系和松耦合(是的,我们说的是耦合)。使用OBSERVER,你将体会到模式的美妙!
Page 38
恭喜!
你们团队刚刚赢得一份合同,你们将为Weather-O-Rama公司开发下一代基于网络的天气监控系统。
信件〉〉 工作说明 恭喜您选择为我们公司开发下一代基于网络的天气监控系统。 该天气预报系统从我们所提供的预告的WeatherData对象中提取数据,它跟踪包括气温、湿度和大气压力在内的当前天气数据。希望你们能够创建这样一个程序:初始的时候,它能显示当前天气、天气统计和简单预报数据。系统所有的更新都必须是实时的,因为WeatherData对象需要最新的数据。 此外,该天气系统必须是可以扩展的。Weather-O-Rama想要发布一个显示API,这样,其他的开发者能在此基础上写他们自己的显示部件并且能够正确的把它插入系统。我们希望你们能够为我们提供该API。 此外,我们还有个很好的商业模型:一旦用户开始使用该系统,我们就倾向于用户能够使自己的显示个性化。我们已经把这部分开发的酬劳作为备选准备好了。 我们期待看到你们的设计和系统的alpha版本。 诚挚的祝福 JOHNNY HURRICANE,CEO 注:我们将全天为你们提供WeatherData数据源文件。 |
Page 39
天气监控系统概述
整个系统由三部分构成:气象站(提供实际天气数据的物理设备)、WeatherData对象(它跟踪气象站的数据并更新显示)和显示器(显示当前天气数据)。
图片〉〉略 |
WeatherData对象知道如何从气象站获取更新数据。然后,WeatherData对象更新三个显示部件。
现在,我们的工作就是创建一个程序:使用WeatherData数据对象更新三个显示部件。
Page 40
分析类WeatherData
按照约定,我们第二天早上就收到了WeatherData数据源文件,匆匆看了一眼代码,事情相当直接、简单。
图片〉〉略 Class WeatherData { GetTemperature(); GetHumidity(); GetPressure();
MeasurementsChanged(); //other methods… } |
Page 41
迄今为止我们都知道些什么呢?
从Weather-O-Rama来的说明不够详细,但是我们必须知道我们需要做什么。那么,我们到现在知道些什么呢?
1、 WeatherData有三个Getter方法用于获取三个天气数据值:温度、湿度、大气压。
2、 当任何新的天气数据出现时候,就调用MeasurementsChanged()方法。此时,我们不知道也不关心如何调用该方法,我们只知道这个时候该调用该方法。
3、 我们需要实现三个显示部件,它们使用天气数据:这些显示部件必须在新的数据来的时候更新。
4、 系统必须是可以扩展的,其他开发者能够创建新的个性化显示部件,并且用户能添加和删除多个显示元素。现在,我们只知道初始的三个显示类型当前天气、统计和预报。
Page 42
谈谈我们第一次、关于天气系统的想当然的设计(SWAG:scientific wild a** guess)
下面是我们第一次可能的实现———我们从Weather-O-Rama的代码得到启示并且在MeasurementChanged()方法中添加我们的代码。
Class WeatherData
{
//实例变量声明
Void MeasurementsChanged()
{
//通过调用WeatherData的getter方法获取最新的天气测量数据
Float temp = GetTemperature();
Float humidity = GetHumidity();
Float pressure = GetPressure();
//下面更新显示,传入最新的天气数据
currentConditionDisplay.Update(temp,humidity,pressure);
statisticDisplay.Update(temp,humidity,pressure);
forecastDisplay.Update(temp,humidity,pressure);
}
//其他方法…
}
练习〉〉磨磨你的铅笔尖 基于上面初次的实现,下面哪项是正确的(可多选) A、 我们在面向具体实现编程,而不是接口 B、 添加任何新的显示元素,我们都必须更改现有代码 C、 我们无法在运行时添加或者删除显示元素 D、所有显示部件没有实现一个共有的接口 E、 我们没有封装变化部分 F、 我们在破坏类WeatherData的封装 |
Page 43
我们的实现有什么问题呢?
回想一下我们在第一章学到的概念和准则…
看看方法MeasurementsChanged(),其中加粗斜体的三行就是肯定要变化的部分,我们没有封装它(上面联系的选项B、E);这三行调用中使用相同的输入参数且无返回值,因此可以使用一个共有的接口来和所有具体显示元素匹配工作(选项D、A);这三行具体实现了三个显示元素,需要添加和删除元素时候,必须更改这段现有的代码(选项A、B、C);
下面让我们来看看OBSERVER模式,然后返回来应用它来开发天气监控系统。
Page 44
看看OBSERVER模式
你所知道的报刊杂志的订阅工作流程大概如下:
1、 报纸发行商开始发行报纸
2、 你订了一个你喜欢的报纸,然后,每当有新的该种报纸出来时就会邮递给你。只要你还是订阅者,那么就就始终能收到你订的新报纸。
3、 当你不想再看该报纸时候,你可以取消以后的订阅,然后,他们就停止给你邮递。
4、 只要报纸发行商在营业,那么任何人都可以不断地订阅或者取消订阅他的报纸。
Page 45
发行商PUBLISHERS+订阅者SUBSCRIBERS=OBSERVER模式
如果你清楚报纸订阅,你既肯定能够理解OBSERVER模式,它们的区别仅仅是把发行商叫做SUBJECT,把订阅者叫做OBSERVERS。
再走近一些看看该模式
图〉〉略 Subject对象管理着一些数据; Observers向Subject订阅了数据(或者在Subject注册),当Subject的数据改变的时候,就会通知所有Observers; 没有注册的不是Observer,他不会接到数据更改的通知。 |
Page 46~47
OBSERVER模式一日游
1、 一个Duck对象过来告诉Subject它想成为一个Observer;
Duck觉得当Subject内部数据改变时候它就会发出新的数据挺神奇的。
2、 现在Duck对象成为了一个Subject官方承认的Observer
Duck首先要进行身体检查……它现在已经被登记在册,它期待着下一次通知从而能够获取新的数据。
3、 Subject有了新的数据
现在Duck和其他的Observers 都收到了新的数据
4、 Mouse对象要求注销掉自己的Observer身份
因为Mouse作为Observer已经好几年了,它感觉累了因此它决定是时候退出了。
5、 Mouse退出了,现在它不是Observer了
Subject同意了Mouse的请求,并且把它从登记册中划掉了。
6、 Subject又有了新的数据
所有的Observer都接到了通知,除了Mouse,因为它已经不是Observer了。但是我告诉你个秘密,Mouse私底下还是很思念以前的日子,因此它可能不久又会要求重新成为一名Observer。
Page 48~50
短剧:a subject for observation
在这个短剧中,两个整日幻想的软件开发者碰到了一个现实生活中的职业中介者。
1、 我是Ron,我想找份Java开发的工作,我有5年的经验…
2、 哦,你和其他人都一样,小家伙。我把你记在我的Java开发者列表中了,不要再给我打电话了,有消息我会给你打的。
3、 我是Jill,我写过很多EJB系统,我愿意从事任何你能找到的Java开发工作。
4、 我记下你的名字了,如果我有新的职位,你会和其他人一起收到通知的。
5、 Ron和Jill的日子仍然和以前一样,如果有Java工作职位,他们会和其他人一起收到通知,毕竟他们都是注册登记过的。
6、 所有Observer注意,现在JavaBeans-R-US有一个公开竞聘的Java职位,快些行动吧,不要错过!!~~~~~~~~~哈哈哈,账户上的数目又要升了!!
7、 Ron:谢谢,我会尽快把我的简历递过去
Jill:这个家伙真是奇怪,谁需要他,我要自己找工作。
8、 Jill:哈喽,你可以把我从你那里注销了,我找到工作了
9、 啊??你给我记住,Jill,你不要想在这个城市找到任何和我有一丁点联系的工作,你已经被我删除了。
10、两周后……
Jill快活的生活着,她不再是个Observer了。并且她收到了一大笔津贴,因为公司不需要给中介支付钱了。
而我们的Ron现在又是怎样呢?我们听说他现在也是个职业中介者,不仅仅是个Observer,还有自己的登记册,他用它来通知在他那里注册的Observer。Ron是集Subject和Observer身份于一身!
Page 51
模式OBSERVER的定义
当你在尝试理解OBSERVER模式的时候,由报刊发行商和订阅者组成的报刊业是个很好的类比例子。
你在书本中通常看到的OBSERVER的典型定义如下:
模式OBSERVER定义了多个对象之间一对多的依赖关系,其中,当一个对象改变了状态,它的所有依赖者都会自动得到通知并更新自身。
让我们再看看该模式的定义:
Subject和Observers定义了一对多的关系。Observers都依赖着Subject,因此当Subject的状态改变时候,他们都会得到通知。根据依赖关系的类型(是PUSH/PULL??),Observers还可以使用新的状态值更新自身。
对于我们所看到的实现该模式的不同方式,它们都包含SUBJECT和OBSERVER接口。
详情见下:
Page 52
OBSERVER模式的类图
图中注释:
1、 Subject接口:对象使用该接口注册本身为Observer或者注销自己的Observer身份。
2、 每个Subject都可以有很多Observer,即一对多的关系
3、 Observer接口:所有潜在的(已经是或者将要是)Observer都必须实现该接口。当Subject的状态改变的时候,调用该接口中的唯一方法Update()
4、 一个具体的Subject总是实现Subject接口。当状态改变的时候,调用NotifyObservers()方法通知所有Observer更新
5、 具体的Subject还可以有存取状态的函数,甚至更多
6、 具体的Observer可以是任何的实现了Observer接口的类,每个具体的Observer向一个具体的Subject登记,以此获取更新的数据。
必须提及的问题:
Q1:一对多关系中什么是必须的?
A1:在模式OBSERVER中,Subject是包含和控制状态的对象,因此,必须有一个拥有状态的Subject对象。另外一个方面,所有Observers使用这些状态,即使它们不用于这些状态数据。有很多的Observer依赖Subject通知它们状态什么时候变化了。因此,一对多就是 ONE Subject 对应 MANY Observers。
Q2:二者的依赖关系如何体现呢?
A2:因为Subject是数据的所有者,Observers依赖Subject,当Subject的数据更改的时候,它们使用这些数据更新本身。这比让很多对象控制相同的数据更加清晰,更加OO。
Page 53
强大的松耦合
当两个对象松耦合,它们可以互相影响,但是互相了解很少。
OBSERVER模式中的Subject和Observers之间就是松耦合的。
为什么呢?
1、 关于任何一个具体的Observer,Subject所知道的唯一信息就是:该具体Observer实现了接口Observer。它也不需要知道Observer的具体类型、它作些什么或者其他关于它的信息。
2、 我们可以在任意时候添加新的Observers。因为Subject唯一依赖的就是实现Observer接口的对象的列表,这样我们可以在任何我们想的时候添加新的Observer。实质上,我们可以在运行时使用一个Observer代替另一个Observer并且Subject仍然会正常运转。同理,我们可以在任意时刻注销Observers。
3、 当我们添加新的Observer类型时候,不需要修改Subject。假设有个新的Observer类型,我们没有必要修改Subject去适应该新类型,我们所要做的只是该新类型实现了Observer接口并且已经向Subject登记为Observer了。Subject不用管Observer的具体类型,它会给所有的实现Observer接口的对象发送通知。
4、 我们可以相互独立的重复使用Subject和Observer。他们之间是非紧耦合的,因此重用很方便。
5、 改变具体的Subject和Observer并不会影响对方。唯一的限制是二者分别实现Subject和Observer接口。
设计原则:当对象间互相影响的时候,努力设计为松耦合!
松耦合允许我们建构高柔性的OO系统,因为通过最小化对象间的依赖关系,系统可以轻松的处理变化。
Page 54
练习〉〉磨磨你的铅笔尖 在继续学习之前,尝试着勾勒出我们在天气监控系统需要实现的类,包括WeatherData类和它的显示元素。确保你的类图能够反映出所有的类是如何结合的并且体现出如何让其他开发者实现他们自己的显示元素。 如果你需要小小的帮助,读读下一页;你的同事们已经开始讨论如何设计天气监控系统。 答案(简,图略):有三个接口:Subject和Observer是OBSERVER模式所必须的,此外有个接口Display,它用来为将来的开发者开发自己的显示元素提供扩展准备,只要显示元素实现了该接口和Observer接口,就可以轻松的插入到该天气系统了。 |
Page 55
小房间内的探讨
回到天气监控系统工程上,你的同事已经开始详细考虑该问题了:
Mary:好了,前面的讲解告诉我们要使用OBSERVER模式!
Sue:是的……但是我们如何应用它呢??
Mary:Hmm,让我们再回顾一下该模式的定义:OBSERVER定义了对象间的一对多的依赖关系:当一个对象的状态改变的时候,所有依赖者都被通知到并且使用新的状态数据自动更新他们。
Mary:你想想,这个定义说的很有道理。我们的天气系统中,WeatherData类是ONE,显示元素是MANY,它们使用WeatherData的数据。
Sue:对,WeatherData的确包含状态……温度、湿度和气压,这些是肯定会变化的。
Mary:不错,当这些状态数据改变的时候,我们必须通知所有显示元素,然后他们就能使用这些新的数据作它们要做的事情了。
Sue:太爽了,我想我现在我懂得如何应用OBSERVER模式到我们的天气监控系统中去了。
Mary:但是我们还要考虑一些事情,有些我还没有完全理解。
Sue:比如像…?
Mary:像下面这个,我们如何让显示元素得到天气数据呢?
Sue:好的,让我回想一下OBSERVER模式,如果我们把WeatherData作为Subject,把显示元素作为Observer,那么显示元素将会向WeatherData对象登记他们,然后他们就可以获取想要的数据了,不是吗??
Mary:对呀….一旦天气系统知道有个显示元素,它就可以调用某个方法去给显示元素传递新的数据。
Sue:我记得每个显示元素都是有些区别的……因此,我想可以提取出一个共有的接口。即使每个显示组件有不同的类型,但是他们必须实现该共有的接口,这样,WeatherData对象才会知道如何给他们发送数据。
Mary:我知道你的意思。你是说每个显示都有个Update()方法,这样WeatherData对象调用它来传递数据。
Sue:Update()方法定义在接口中,所有元素实现它……
Page 56
设计天气监控系统
下面的类图和你的有什么不同呢??
图中注释略。
Page 57
实现天气监控系统
按照Mary和Sue的引导,我们开始实现前面类图所示效果。本章后面你可以看到Java提供一些内置的OBSERVER模式的支持,但是,我们仍然决定亲自实现我们自己的东东。有时使用Java内置比较方便,但是很多时候,构建自己的将会有更高的柔性(并且相当简单)。因此,让我们从三个接口开始吧。
Public interface ISubject
{
//这两个方法都有一个Observer参数,表示该参数要被添加或者删除
Public void registerObserver(IObserver o);
Public void removeObserver(IObserver o);
//当状态改变时候,调用该函数通知所有Observer
Public void notifyObservers();
}
Public interface IObserver
{
//在这里我们暂时按照Mary和Sue的讨论,所有的天气数据直接传递给Observer
//当Subject的数据改变时候,就把这些新的数据给Observers传递
Public void Update(double temp,double humidity,double pressure);
}
Public interface DisplayElement
{
//任何具体显示元素都必须实现该接口
Public void display();
}
想想看(Brain Power)〉〉 Mary和Sue认为直接将数据传送给Observers是最简捷的更新状态的方法。你认为她们这样做明智吗?提示:在程序中是否存在一个区域在将来可能会发生变化。如果它真的变化了,是应把变化部分封装起来还是在代码的各个部分去改动。 你能过想出其他方式来解决传递更新的状态数据给Observers的问题吗? 不要急,我们将在完成我们的初始实现后,再回来讨论这个设计问题上!! |
Page 58
实现天气监控系统中的Subject接口
还记得我们在本章开始时候第一次实现的类WeatherData?你可能想刷新一下你的记忆。现在就是在脑子中回顾一下OBSERVER模式然后做些什么的时候了。
切记:我们下面的代码中不提供IMPORT和PACKAGE的声明。如果你想获取完整的代码,你可以从站点WickedSmart下载。你可以在前面的xxxiii页的本书介绍中找到该站点的完整URL。
//WeatherData实现ISubject接口
Public class WeatherData implements ISubject
{
//首先在WeatherData中添加一个列表来登记所有的Observer,这个列表在类的构
//造函数中创建
Private ArrayList observers;
Private double temp;
Private double humidity;
Private double pressure;
Public WeatherData()
{
Observers = new ArrayList();
}
Public void registerObserver(IObserver o)
{
//当一个Observer注册时候,只要把它添加在列表
Observers.add(o);
}
Public void removeObserver(IObserver o)
{
//同样,当注销一个Observer的时候,只要把它从列表删除即可
Int i = observers.indexof(o);
If(i>0) observers.remove(i);
}
Public void notifyObservers()
{
//这是最有趣的部分,在这里告诉所有Observer关于状态的信息
//因为所有Observer都实现接口IObserver中的Update方法
//因此,我们知道如何通知Observer
For(int i=0;
{
IObserver observer = (IObserver)observers.get(i);
Observer.update(temp,humidity,pressure);
}
}
Public void meseaurementsChanged()
{
//当我们从气象站获取更新的数据时候,我们就通知Observer
notifyObservers();
}
Public void setMeasurements(double temp,double humidity,double pressure)
{
//好的,让我们给每本书都附带一个小型的气象站,但是出版商不会同意的,
//因此,我们不是从气象站读取真实的数据,我们用自己的方法来测试显示元
//素。如果你想玩的更有意思,你可以从网络站点上获取最新天气数据
This.temp = temp;
This.humidity = humidity;
This.pressure = pressure;
MeseasurementsChanged();
}
}
Page 59
现在,我们来构建这些显示元素
我们现在已经有了WeatherData类了,是时候构建显示元素了。Weather-O-Rama预定了三个显示元素:当前天气显示、天气统计显示和天气预报显示。让我们先看看当前天气显示元素;一旦你感觉这个显示元素还可以,你就可以从本书的代码目录中获取另外两个显示元素,你会发现它们非常相似。
//这个类实现了两个接口,实现IObserver是为了它能从WeatherData获取最新的数据
//实现IDisplayElement接口,这是所有显示元素必须作到的
Public class CurrentConditionsDisplay import IObserver,IDisplayElement
{
Private double temp;
Private double humidity;
Private ISubject weatherData;
Public CurrentConditionsDisplay(ISubject wd)
{
//构造函数有个ISubject输入参数,我们使用它把显示元素本身登记为Observer
This.weatherData = wd;
Wd.registerObserver(this);
}
Public void Update(double temp,double humidity,double pressure)
{
//当调用Update时候,首先保存这些新的数据,然后调用显示
This.temp = temp;
This.humidity = humidity;
Display();
}
Public void Display()
{
//这里的显示仅仅是把信息打印到控制台上
System.out.println(“Current Conditions:”+temp+”F degrees and ”+humidity+”
%humidity”);
}
}
必须要提及的问题
Q1:方法Update是调用显示的最佳之地吗?
A1:在这个简单的例子中,看起来在数据改变时候调用显示是当然之选。但是,你的问题问的很好,的确有一些更好的方法来调用显示。我们将在模式MODEL-VIEW-CONTROLLER的讲解中看到这些更好的方式。
Q2:为什么你在WeatherData中保留一个指向Subject的引用?但是看起来再构造函数之后你们就没有再使用这个引用!?
A2:是的,没有使用第二次,但是在我们要注销Observer身份的时候,就需要一个指向Subject的引用了??????????要吗,不是仅仅从列表中删除吗???。]
Page 60
驱动天气监控系统
1、 首先,创建一个测试框架
天气监控系统核心已经准备就绪,现在仅仅需要一些代码把所有东西串在一起了。下面是我们的首次尝试。在本书的后面我们将返回来讨论这个问题,在那里我们将通过一个配置文件将所有组件轻松的组合在一起!
Public static void main(String args)
{
WeatherData wd = new WeatherData();
CurrentConditionDisplay ccd = new CurrentConditionDisplay(wd);
//我们略去了其余两个显示元素,然后仿真新的天气数据
Wd.SetMeasurements(80,64,30.4);
Wd.SetMeasurements(89,70,32.0);
}
2、 运行我们的代码,看看OBSERVER模式的魔力
图略
Page 61
练习〉〉磨磨你的铅笔尖 Johhny Hurricane,Weather-O-Rama公司的总裁刚刚打个电话,他们必须要显示Heat Index。下面是计算Heat Index的细节: Heat Index是一个结合温度和湿度来计算表面温度的值,又称热指数,即我们实际感受到有多热。使用温度和相对湿度Relative Humdity=RH,公式如下: Heat index = (float)((16.923 + (0.185212 * t) + (5.37941 * rh) - (0.100254 * t * rh) +
(0.00941695 * (t * t)) + (0.00728898 * (rh * rh)) +
(0.000345372 * (t * t * rh)) - (0.000814971 * (t * rh * rh)) +
(0.0000102102 * (t * t * rh * rh)) - (0.000038646 * (t * t * t)) + (0.0000291583 *(rh * rh * rh)) + (0.00000142721 * (t * t * t * rh)) +
(0.000000197483 * (t * rh * rh * rh)) - (0.0000000218429 * (t * t * t * rh * rh)) + 0.000000000843296 * (t * t * rh * rh * rh)) -
(0.0000000000481975 * (t * t * t * rh * rh * rh))); 现在开始抄这个公式吧! 开个玩笑。不要担心,你不需要逐字的抄写这个公式,你只需要从wickedlysmart.com上的heatindex.txt中拷贝它。 这个公式是什么原理呢??你必须先读读《深入浅出气象学》这本书,或者去问问那些在国家气象站工作的人,还有,你可以上Google查查!~ 当你创建了HeatIndex的显示之后,运行程序你可以看到下面的运行结果! 图略 |
Page 62~63
火炉旁的聊天
今晚的话题是关于Subject和Observers争论如何才是给Observer传递状态信息的正确方式。
Subject:我很高兴我们两能有私下谈话的机会!
Observer:真的吗?我原以为你根本不关心我们这些Observer。
Subject:我在做我份内工作,不是吗?我总是告诉你们都发生些什么…我不知道你们的具体身份并不代表我不关心你们。此外,我所知道的关于你们最重要的东西就是你们实现了接口Observer。
Observer:是的,但那只是我们身份的一小部分,而且我们知道你很多信息…
Subject:哦?比如??
Observer:好的,你总是传递你的状态信息给我们,因此我们能够看到你内部都发生了什么事情。但有时候你传递信息给我们会让我们有点烦。
Subject:对不起,我打断一下。我只是把我的状态在通知你们之时一同传递过去,然后你们这些懒惰的Observer才能知道发生了什么事情啊!
Observer:等等,我也打断一下;首先,我声明一下啊,我们并不懒,我们除了接收你认为很重要的通知之外,还有很多事情要做!Subject先生,我想说的第二点是:为什么你不能我们自己从你内部取出我们想要的状态信息呢?那不比你把所有的信息传递给所有Observer要好的多吗??
Subject:哦?我想那样大概也可以,但是,那样我就必须完全公开我自己,然后让你们在我的内部随意挑选你们想要的信息。这样会很危险的,不是吗?我不能允许你们进入我的内部然后偷偷摸摸的查看我所有的信息。
Observer:但是,你为什么不提供一些共有的Getter方法来帮助我们获取我们需要的状态信息呢?
Subject:是的,我可以让你们提取PULL我的状态信息,但是那样你们难道不会感到不方便吗?那样,你每次进入我想要获取你的所需,就不得不调用很多的方法。这就是为什么我比较倾向于发布PUSH…这样,在每个通知中你们就可以得到所有你们想要得到的信息。
Observer:不要这样自负吗!我们这些Observer的具体身份是千差万别的,你无法预计到我们所有人的需求。就让我们自己去你那里获取信息。那样的话,如果我们当中的一些人仅仅只需要少数状态信息,就不需要被强迫获取所有的状态信息,这样还可以让后面的修改更加容易:比如,你要扩展你自己,加入了更多的一些新状态信息,此时,如果你同意使用提取PULL的方式,你就不需要去遍历所有Observer的Update调用了,你仅仅需要添加些Getter方法来让我们自己去获取这些额外添加的新的信息。
Subject:是啊,我好像看到了这两种方式各有千秋。我注意到Java的内置OBSERVER模式允许你们在提取PULL或者发布PUSH两种方式间选择。
Observer:真的吗?我认为我们下一步就去看看这个内置的….
Subject:非常好…或许让我看个提取PULL方式的典型例子之后,我会改变我的想法。
Observer:你说什么?难道我们已经就什么东西达成一致了?我猜想希望总是有的……
Page 64
使用Java内置的OBSERVER模式
现在,我们已经写好了自己的OBSERVER模式代码。Java有好多API都内置对OBSERVER模式的支持。最常用的是在Java.util包中的Observer接口和Observable类,它们分别和Observer接口和Subject接口非常相似,但是还提供很多额外的功能。你还可以任意选择提取PULL或者发布PUSH方式来更新所有Observer,随后你就可以看到。
为了对Java内置的OBSERVER模式有更高层次的理解,下面是运用Java内置的OBSERVER模式对天气监控系统所做的重新OO设计的类图。
图略〉〉
Page 65
Java的内置OBSERVER模式如何工作呢?
该内置的OBSERVER模式和我们起初实现天气监控系统的OBSERVER实现代码工作原理有些不同。最明显的不同之处是WeatherData(即Subject)现在是继承Observable类以及该类内的所有方法。下面是我们如何使用Java的OBSERVER模式版本:
1、 把一个对象登记为Observer……
根通常一样,对象首先实现Observer接口(这次是Java包内的Observer接口),然后调用Observable对象的方法addObserver(),这样对象有了Observer身份。同理,注销Observer身份,调用deleteObserver();
2、 Observable发送通知……
首先,让对象继承类Observable,然后发送通知要分为两步:首先调用SetChanged()方法来标志对象的状态发生了变化。然后,调用任意一个通知方法:NotifyObservers()或者NotifyObservers(Object arg)。
3、 Observer接收通知……
Observer实现了Update方法,但是和前面的方法型构有些区别:
Update(Observable o,Object arg);
这里发送通知的Subject作为参数传递给Observer,注意2中发送通知的方法和Update方法一样都有一个参数Object arg,这个参数是数据对象:在NotifyObserver方法中有该参数表示为发布PUSH方式传递数据信息给所有Observers,该参数为Null时候,表示PULL提取方式,Observer从Subject中提取数据信息。这就对应了Update方法的另外一个参数,当arg为Null时候,Observer通过参数Observable o的Getter方法提取o中的数据信息。
如何工作呢?让我们重新以Java内置OBSERVER模式实现天气监控系统,如下面所示。
图〉〉 等等,在我们继续之前,先告诉我为什么需要一个叫做SetChanged()的方法,以前我们并不需要它! |
方法SetChanged()是用来标示Subject中的状态发生了变化,如果调用NotifyObservers()之前,调用了该方法,那么才去更新所有Observer,反之,如果没有调用SetChanged方法,就表示不需要通知Observer,让我们看看Observable中如何结合SetChanged方法和NotigyObservers方法使用的内幕:
代码内幕〉〉(behind the scenes)
SetChanged()
{
//状态变化标志
Changed = true;
}
Void NotifyObservers(Object data)
{
If(Changed == true)
{
//伪代码,如果变化标志为TRUE,那么通知所有Observer更新,并重置变化标志为False。
For each Observer in the list
Update(this,data);
Changed =false;
}
}
为什么这个方法有必要呢?因为该方法可以给你一定的灵活性来优化你的通知条件。比如,在我们的天气监控系统中,如果我们的测量设备足够灵敏,那么温度仪上的指示会不断地有微小的零点几度的变化。这将导致我们的WeatherData对象不停地给所有Observer发送通知。实际上,我们可能是在温度变化大于0.5度的时候才发送通知,这时在SetChanged()方法中检测是否达到该条件,然后相应设置变化标志。
你或许不经常使用该方法,但是在某些地方,你肯定需要。如果这个功能对你有用,那么可能还想要个方法ClearChanged(),它用来把变化标志重置为False,另外可以有个方法叫做IsChanged()调用它可以帮助你查询当前变化标志值。
使用Java内置OBSERVER模式重写天气监控系统
首先,使用Java包的Observable重写WeatherData类。
//导入Java包
Import java.util.Observable;
Import Java.util.Observer;
//现在是继承基类Observable,而不是接口Subject
//现在,我们不需要再去自己写代码登记、注销和通知所有的Observer,这些都在基类Observable中
Public class WeatherData extends Observable
{
Private float temp;
Private float humidity;
Private float pressure;
//构造函数无需再为列表分配空间了
Public WeatherDatra(){}
Public void MeasurementsChanged()
{
//我们首先设置变化标志为true然后调研通知方法
SetChanged();
//我们的通知参数是null,因此,WeatherData采用提取PULL方式传递数据
NotifyObservers(null);
}
Public void setMeasurements(double temp,double humidity,double pressure)
{
This.temp = temp;
This.humidity = humidity;
This.pressure = pressure;
MeseasurementsChanged();
}
//下面三个Getter方法是Observer用于从WeatherData中提取PULL数据的方法
Public double GetTemp()
{
Return temp;
}
Public double GetHumidity()
{
Return humidity;
}
Public double GetPressure()
{
Return pressure ;
}
}
现在让我们重新CurrentConditionDisplay类。
//导入Java包
Import java.util.Observable;
Import Java.util.Observer;
//此处是实现implements接口Observer和DisplayElement
Public class CurrentConditionDisplay implements Observer , DisplayElement
{
Observable observable;
Private double temp;
Private double humidity;
//构造函数有个参数,Display将向它登记为Observer身份
Public CurrentConditionDisplay(Observable observable)
{
This.observable = observable;
Obsrevable.addObsrver(this);
}
//Update现在有两个参数:obs和可选的arg,arg是数据参数
Public void Update(Observable obs,Object arg)
{
If(obs instanceof WeatherData)
{
WeatherData weatherData = (WeatherData)obs;
//Observer用Getter方法获取Subject即Observable中的所需数据,这就是PULL方式
This.temp = weatherData.GetTemp();
This.humidity = weatherData.GetHUmidity();
Display();
}
}
Public void Display()
{//略……}
}
Page 69
代码片断接龙
类ForeClass的代码片断现在全贴在冰箱上。你能把它们重新排序一下吗?有些括号掉在了地上,如果在拼接过程中需要,你可以任意添加!
代码片断〉〉略
Page 70
运行新的代码
图〉〉略
嗯,你注意到这里的运行结果和前面的有什么不同吗?再仔细看看……
你看到的计算结果是相同的,但是,令人费解的是,结果输出的顺序不同了。为什么会这样,在继续之前认真想想……
永远不要以来OBSERVER模式中发送通知的计算顺序
Java内置的OBSERVER模式支持类Observable所实现的NotifyObserver方法和我们自己的实现在发送通知的顺序上有些区别。那么那个正确呢?这里没有对错,区别仅仅在于二者的实现方式不同。
但是,我们如果在实现中依赖一个具体的通知顺序,此时就不正确。为什么,如果你更换了你自己的实现为Java内置的支持,那么你的依赖原有计算顺序的程序将会得到错误的结果。这显然和我们前面考虑的松散耦合相违背。
Page 71
Java内置Observable类的设计缺陷
图〉〉略 那么Java内置的java.util.Observable是不是没有遵守我们在第一章所提到的OO设计原则:对接口编程,而不是实现? |
是的,问得好!正如你注意到的,Observable是个类,而不是接口。更糟糕的是,它甚至没有实现某个接口。不幸的是,Observable这些有缺陷的实现限制了它的使用和重复使用。这并不是说它没用,但是有些缺陷必须指出:
1、 Observable是个类
你已经知道这样违背了我们的设计原则,但是它到底会引起什么样危害呢?
第一:Observable是个类,你必须继承它。这就意味着你不能把Observable的功能加在一个已经扩展了另外一个基类的子类了!(Java中只允许单继承,但是在C++中这是可以实现的)。这就限制了它的潜在的重复使用性(这不正是我们要求使用模式的第一个原因吗)。
第二:因为它不是个接口,你就不可能让自己的实现和Java内置的API良好的协作。你肯定没有想出用另外一个好点子来替代Java内置的实现(告诉你吧,多线程的实现)。(Nor do you have the option of swapping out the java.util implementation for another (say ,new ,multithread implementation))。
2、 Observable没有公开关键的方法
Observable中那个SetChanged方法是protected。这样就意味着你不能调用该方法,除非你继承了Observable。这还意味着你都不能创建一个Observable实例然后把它作为你自己对象一个composition,你必须从它派生。这种设计违背了第二个OO设计原则:尽量使用组合Composition代替继承inheritance。
3、 那么我们该怎么办?
如果你继承了Observable,它将满足你的一些需求。另外一方面,你可能需要构建自己的实现。在任何情况下,在你良好理解了OBSERVER模式之后,你可以处在有利的一面,不论你使用Java内置的还是自己的实现
Page 72
JDK中的其它内置的对OBSERVER模式的支持
Java.util.Observer/Observable并不是唯一一个在JDK中内置支持模式OBSERVER;JAVABEANS和Swing都有它们各自对模式OBSERVER的支持版本。学到这里,你已经对OBSERVER模式很了解了并且能够应用这些已有的API到自己的应用了。下面我们以一个简单的Swing例子作简单说明。
如果你对JAVABEANS中的OBSERVER模式很好奇,那就看看PropertyChangedListener接口。
一些背景知识…
让我们看看Swing的API-JButton。如果你看看JButton的基类AbstractButton,你会发现它有很多添加和删除监听者的方法。这些方法允许你登记或者注销Observer身份,在Swing中它也被称为监听者Listener,它们用于监听Swing组件发生的各种事件。比如:ActionListener可以监听所有在Button上可能发生的所有动作,像按键的动作。在Swing中到处都是这样的监听者。
一个简单的命运转折点程序(a life-changing app)
我们的这个程序相当简单。现在,你按了一个Button,它说:我该做这件事情吗?此时Listener(Observer)监听到你按键了,它会按照它所想的方式回答你的问题。我们实现了两个监听者:天使监听者和恶魔监听者。下面是这个程序的反应:
图〉〉略
Page 73
程序代码如下……
这个命运转折点程序只需要很少的代码。我们所要做的仅仅是常见一个JButton对象,把它添加到JFrame然后建立我们的监听者。我们使用Swing内部类作为监听者,这在Swing编程中很通用。如果你对Swing的内部类不熟悉,那么就看看《深入浅出Java》这本书中关于对GUI的描述。
代码〉〉略
Page 74
给你的设计工具箱里添些新的东西
OO基础
抽象、封装、多态、继承
OO原则
封装变化
使用组合而不是继承
面向接口编程而不是实现
//下面是你最新的原则:记住,松散耦合可以让你的设计更有弹性和柔性
在相互影响的对象间努力做到松散耦合设计
OO模式
策略模式
//下面是个新的模式:用于在一系列对象间以松散耦合的方式进行状态信息的通信。
//一些关于该模式的问题我们还没有讲解,这在后面MVC模式将会给出。
观测者模式:定义了一系列对象间的一对多的依赖关系。当一个对象的状态发生改变的时候,所有依赖者都会自动得到通知和更新。
关键知识点:
1、 OBSERVER模式定义了许多对象间的一对多的关系
2、 Subject,或者称为Observable,使用一个通用的接口更新所有Observer
3、 Observer和Subject间是松散耦合的。因为Subject仅仅知道Observer实现了Observer接口,其余的一概不知。
4、 传递状态信息的方法有:PUSH和PULL,PULL被认为更好一些。
5、 不要依赖某个具体的通知Observer的顺序
6、 Java有很多内置对模式OBSERVER支持的实现,包括通用的java.util.Observable
7、 注意Java的内置Observable实现的设计有缺陷
8、 在需要你自己的实现时,不要犹豫
9、 像很多GUI框架一样,Swing大量使用了OBSERVER模式
10、你可以在其它诸如JAVABEAN、RMI等地方发现OBSERVER模式的应用。