第1章 监听模式

1.1 从生活中领悟监听模式

1.1.1 故事剧情—幻想中的智能热水器

刚刚大学毕业的 Tony只身来到北京这个大城市,开始了北漂生活。但刚刚毕业的他身无绝技、包无分文,为了生活只能住在沙河镇一个偏僻的村子里,每天坐着程序员专线(13号线)穿梭于昌平区与西城区……

在寒冷的冬天,Tony坐2个小时的“地铁+公交”回到住处,拖着疲惫的身体,准备洗一个热水澡暖暖身体,奈何简陋的房子中用的还是20世纪90年代的热水器。因为热水器没有警报,更没有自动切换模式的功能,所以烧热水必须得守着,不然时间长了成“杀猪烫”,时间短了又“冷成狗”。无奈的 Tony 背靠着墙,头望着天花板,深夜中做起了白日梦:一定要努力工作,过两个月我就可以自己买一个智能热水器了,水烧好了就发一个警报,我就可以直接去洗澡。还要能自己设定模式,既可以烧开了用来喝,又可以烧暖了用来洗澡……

1.1.2 用程序来模拟生活

Tony陷入白日梦中……他的梦虽然不能在现实世界中立即实现,但在程序世界里可以。程序来源于生活,下面我们就用代码来模拟Tony的白日梦。

源码示例1-1 模拟故事剧情

测试代码:

输出结果:

1.2 从剧情中思考监听模式

这个代码非常简单,水温在50℃~70℃时,会发出警告:可以用来洗澡了!水温在100℃时也会发出警告:可以用来饮用了!在这里洗澡模式和饮用模式扮演了监听的角色,而热水器则是被监听的对象。一旦热水器中的水温度发生变化,监听者就能及时知道并做出相应的判断和动作。这就是程序设计中监听模式的生动展现。

1.2.1 什么是监听模式

Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and updated automatically.

在对象间定义一种一对多的依赖关系,当这个对象状态发生改变时,所有依赖它的对象都会被通知并自动更新。

监听模式是一种一对多的关系,可以有任意个(一个或多个)观察者对象同时监听某一个对象。监听的对象叫观察者(后面提到监听者,其实就指观察者,两者是相同的),被监听的对象叫被观察者(Observable,也叫主题,即Subject)。被观察者对象在状态或内容(数据)发生变化时,会通知所有观察者对象,使它们能够做出相应的变化(如自动更新自己的信息)。

1.2.2 监听模式设计思想

监听模式又名观察者模式,顾名思义就是观察与被观察的关系。比如你在烧开水的时候看着它开没开,你就是观察者,水就是被观察者;再比如你在带小孩,你关注他是不是饿了,是不是渴了,是不是撒尿了,你就是观察者,小孩就是被观察者。观察者模式是对象的行为模式,又叫发布/订阅(Publish/Subscribe)模式、模型/视图(Model/View)模式、源/监听器(Source/Listener)模式或从属者(Dependents)模式。当你看这些模式的时候,不要觉得陌生,它们就是监听模式。

监听模式的核心思想就是在被观察者与观察者之间建立一种自动触发的关系。

1.3 监听模式的模型抽象

1.3.1 代码框架

模拟故事剧情的代码(源码示例1-1)还是相对比较粗糙的,我们可以对它进行进一步的重构和优化,抽象出监听模式的框架模型。

源码示例1-2 监听模式的框架模型

1.3.2 类图

上面的代码框架可用图表示,如图1-1所示。

Observable是被观察者的抽象类,Observer是观察者的抽象类。addObserver、removeObserver分别用于添加和删除观察者,notifyObservers 用于内容或状态变化时通知所有的观察者。因为Observable的notifyObservers会调用Observer的update方法,所有观察者不需要关心被观察的对象什么时候会发生变化,只要有变化就会自动调用update,所以只需要关注update实现就可以了。

图1-1 监听模式的类图

1.3.3 基于框架的实现

有了源码示例1-2的代码框架之后,我们要实现示例代码的功能就更简单了。我们假设最开始的示例代码为Version 1.0,下面看看基于框架的Version 2.0吧。

源码示例1-3 Version 2.0的实现

测试代码不用变,读者可以自己跑一下,会发现输出结果和之前的是一样的。

1.3.4 模型说明

1.设计要点

在设计监听模式的程序时要注意以下几点。

(1)要明确谁是观察者谁是被观察者,只要明白谁是应该关注的对象,问题也就明白了。一般观察者与被观察者之间是多对一的关系,一个被观察对象可以有多个监听对象(观察者)。如一个编辑框,有鼠标点击的监听者,也有键盘的监听者,还有内容改变的监听者。

(2)Observable 在发送广播通知的时候,无须指定具体的 Observer,Observer 可以自己决定是否订阅Subject的通知。

(3)被观察者至少需要有三个方法:添加监听者、移除监听者、通知Observer的方法。观察者至少要有一个方法:更新方法,即更新当前的内容,做出相应的处理。

(4)添加监听者和移除监听者在不同的模型称谓中可能会有不同命名,如在观察者模型中一般是addObserver/removeObserver;在源/监听器(Source/Listener)模型中一般是attach/detach,应用在桌面编程的窗口中还可能是attachWindow/detachWindow或Register/UnRegister。不要被名称弄迷糊了,不管它们是什么名称,其实功能都是一样的,就是添加或删除观察者。

2.推模型和拉模型

监听模式根据其侧重的功能还可以分为推模型和拉模型。

推模型:被观察者对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。一般在这种模型的实现中,会把被观察者对象中的全部或部分信息通过update参数传递给观察者(update(Object obj),通过obj参数传递)。

如某App的服务要在凌晨1:00开始进行维护,1:00—2:00所有服务会暂停,这里你就需要向所有的App客户端推送完整的通知消息:“本服务将在凌晨1:00开始进行维护,1:00—2:00所有服务会暂停,感谢您的理解和支持!”不管用户想不想知道,也不管用户会不会在这期间访问App,消息都需要被准确无误地发送到。这就是典型的推模型的应用。

拉模型:被观察者在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到被观察者对象中获取,相当于观察者从被观察者对象中拉数据。一般在这种模型的实现中,会把被观察者对象自身通过 update 方法传递给观察者(update(Observable observable),通过observable 参数传递),这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

如某App有新的版本推出,需要发送一个版本升级的通知消息,而这个通知消息只会简单地列出版本号和下载地址,如果需要升级App,还需要调用下载接口去下载安装包完成升级。这其实也可以理解成拉模型。

推模型和拉模型其实更多的是语义和逻辑上的区别。我们前面的代码框架,从接口[update(self,observer,object)]上你应该可以知道是同时支持推模型和拉模型的。作为推模型时,observer可以传空,推送的信息全部通过object传递;作为拉模型时,observer和object都传递数据,或只传递observer,需要更具体的信息时通过observer引用去取数据。

1.4 实战应用

在互联网广泛普及和快速发展的时代,信息安全被越来越多的人重视,其中账户安全是信息安全最重要的一个部分。很多网站都会有一个账号异常登录检测和诊断机制。当账户异常登录时,会以短信或邮件的方式将登录信息(登录的时间、地区、IP地址等)发送给已经绑定的手机或邮箱。

登录异常其实就是登录状态的改变。服务器会记录你最近几次登录的时间、地区、IP地址,从而得知你常用的登录地区;如果哪次检测到你登录的地区与常用登录地区相差非常大(说明是登录地区的改变),则认为是一次异常登录。而短信和邮箱的发送机制我们可以认为是登录的监听者,只要登录异常一出现就自动发送信息。

逻辑分析清楚之后就可以设计我们的代码了,首先设计类图,如图1-2所示。

图1-2 登录异常检测机制的设计类图

源码示例1-4 登录异常的检测与提醒

测试代码:

输出结果:

在实际的项目中,用户信息(如用户名、密码)都是放在数据库中的,登录时还要进行用户信息的校验;用户最近几次的登录信息也存在数据库中。这里,为模拟程序简单起见,省去了数据库操作这一步,而且只记录上一次的登录信息到Account对象中。

1.5 应用场景

(1)对一个对象状态或数据的更新需要其他对象同步更新,或者一个对象的更新需要依赖另一个对象的更新。

(2)对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节,如消息推送。

学习设计模式,更应该领悟其设计思想,不应该局限于代码的层面。监听模式还可以用于网络中的客户端和服务器,比如手机中的各种App的消息推送,服务端是被观察者,各个手机App 是观察者,一旦服务器上的数据(如 App 升级信息)有更新,就会被推送到手机客户端。在这个应用中你会发现服务器代码和App客户端代码其实是两套完全不一样的代码,它们是通过网络接口进行通信的,所以如果你只停留在代码层面是无法理解的!