我的实践与反思-观察者模式
Observer Pattern
起因
在我曾经参与的一个某市人行的信息查询系统中,客户的各个科室有不同的业务需求,同时有一些人具有很大的权限,可以查看不同科室的信息与业务,简单说,就是要实现一个功能有些分散和相对独立同时具有灵活的权限管理的一个介于MIS和BI之间的系统。
经过讨论,我们决定采用插件的方式来完成这些操作。针对功能的相对独立性,我们的初步设计是使用公共的平台作为插件的载体,每个插件是一个相对独立的功能。这样的架构有下面好处:
1. 公共的部分可以放在平台,避免重复开发;
2. 耦合性低,一个功能的修改将风险控制在插件内部,避免影响平台和其他功能的稳定;
3. 提高可扩展性,新增的功能可以提供新的插件来完成,既不影响之前的功能,又能利用到之前的成果;
4. 方便升级,只需要将要升级的插件替换一下即可;同样,平台的升级由于接口的稳定,也不会影响到插件;
5. 便于权限控制,可以方便控制某个功能的使用和不使用。
插件的接口相对比较简单:插件在创建的时候会接收平台传给插件一个服务对象,供插件使用平台提供的一些服务;插件提供一些信息,比如插件的名称、用途描述、版本号、供区分每个插件的GUID等等,供平台调用。 总之这是个设计比较简单,粒度比较大的插件架构。
类的设计如下图所示:
客户要求系统的客户端基于Windows 2000和Windows XP平台并且采用WinForm方式;服务端使用Linux+DB2,因此我们将每个插件采用DLL来实现。
遇到问题
插件就简单介绍到这里,下面着重说一下我们的权限控制的设计。
经过和客户的充分沟通,我们确定了权限管理的一些约束条件:
1. 有两位超级管理员,用户名固定,密码为32位~64位之间的系统自动生成的随机字符串,打印在密码封中。强制每个自然月重新生成密码一次;
2. 超级管理员可以相互重置彼此的密码;
3. 超级管理员负责生成角色,每种角色是一组功能的集合;
4. 超级管理员负责生成操作员,操作员初始由某种角色决定权限,管理员可以更改期具体的权限。也就是说角色实际上只是一个可用功能集合的模板,管理员可以在模板的基础上修改操作员的具体权限。
还有一些业务约束就不一一列举了,这些都和本篇的主题无关。
如果不考虑可维护性、可扩展性、可读性,并且假使我们清楚知道系统的每个功能,我们当然可以将所有功能列举,然后请管理员进行勾选,对权限进行控制,但是对于插件系统,我们面临一个困境:
我们永远无法知道将来有谁会使用插件来扩展我们的系统,也永远不知道它会为我们的系统提供什么功能和要求什么样的权限控制。
解决问题
鉴于此,我们决定,由插件自己控制功能是否可用。我们要做的只是一件事情:告诉插件们,“插件们大家好,我是管理员”。或者“HI,Everybody,我是操作员Muhx,归我管的都起来干活了”。
这主要分几个步骤:
1. 在插件接口中增加一个方法,GetFunctionList,要求插件将所有的涉及到权限控制的功能报告出来。也就是说,如果其他人要设计一个被我们的平台调用的插件,他必须实现这个方法,将自己插件里面希望被控制的功能保存到功能列表中,供平台调用;
2. 平台提供注册插件RegisterPlug和删除插件UnRegisterPlug的功能;
3. 平台提供一个界面,将所有插件的功能列举出来供权限配置用,将配置好的功能列表RoleFunctionList传递给插件或者持久化;
4. 平台在用户登录或者更改权限的时候通知所有插件,插件根据配置好的功能列表RoleFunctionList来自己决定是否提供某项功能。
类图如下所示:
这样的设计,很好地解决了问题。
观察者模式
上面的权限管理中使用了观察者模式,复习一下观察者模式的定义:
定义一种对象间的一对多的关系,当一个对象的状态发生变化的时候,所有依赖于它的对象都将得到通知并更新自己。
在上面的例子中,平台(Client)相当于被观察者,当平台的状态发生变化(用户登录或者更改权限)的时候,所有的观察者(Plug)都得到通知,并且根据通知的内容(当前的权限)自动更新自己可被使用的功能。
在观察者模式中,被观察者常被称为主题(Subject),上面的例子中为了实现插件功能,我们增加了一些额外的信息,比如Service等等,比较典型的观察者类图如下图所示:
在观察者模式中,Subject维护一个观察者列表,若状态发生变化就通知所有的观察者,并将变化的值传给每一个观察者,观察者可以根据传过来的值决定是否需要响应Subject的变化。
进一步思考-主动还是被动?
在《我的实践与反思-策略模式》中曾经提到,我们在串口操作中使用了阻塞方式,也就是说上位机发出一个指令后,要等指令回来才继续下一步操作。事实上在底层的设计中,我们用的却是Windows API的非阻塞模式——在发出指令之后我们并没有等待指令的返回。为了知道下位机是否处理完毕,我们在上位机中设置查询线程,每隔一段时间就访问一次下位机,看看是否执行完毕。
站在上位机的角度,我们是主动的,不断地采用轮询的方式来获得下位机的状态。这种方式的好处是能控制和下位机交互的频率、简化了程序的设计、可以给客户提供不同的刷新频率让客户有不同的体验。特别是在仪器仪表的监测中,这种控制尤其重要。例如,对一个电池放电过程中每个时间段的功率进行测算,如果取的时间间隔太小则数据量非常庞大;取的时间间隔太大则不容易精确描绘曲线。我们这种自己控制刷新频率的方法能给客户提供自己抉择的可能。
缺点也是显而易见,最主要的是丧失了实时性,其次是不停的轮询让系统始终处于忙碌状态。
还是站在上位机的角度,假设我们是被动的,不去问下位机是否已经执行完毕,而是让下位机在执行完毕之后通知我们,这样的设计就是一个典型的观察者模式。
上位机是观察者,下位机是主题,在主题发生变化的时候,上位机立刻得到通知,并更新自己(比如将下位机的结果显示出来)。
进一步思考-推还是拉?
采用观察者模式,上位机是观察者,下位机是主题,遇到的最大的一个问题是数据爆炸。由于下位机的执行效率很高,在仪表仪器中,往往采集数据的运算量并不大,因此下位机会以极小的时间得到结果并且将结果返回给上位机。
假设一个数据包是400K,每秒采集十次数据(不考虑传输效率),下位机每次采集结束后都将数据返回给上位机,我们可以知道,这个数据马上就是一个天文数字。同时上位机要处于不停的刷新界面之中,恐怕上位机也会处在极度忙碌甚至假死机状态。
在观察者模式中提供了两种通知方式,一种是主题将数据发给观察者,就是前面的介绍,这种被称为“推”。 另一种是主题只通知观察者数据发生改变,由观察者自己决定是否需要主题的数据,这种被称为“拉”。 通俗一点,“拉”就是“观察者们,我有新数据了,如果需要你们就来取吧”。
要实现拉的效果需要对前面的类图做点改变:
1. 主题提供获取数据信息的Public方法;
2. 观察者要能够引用主题对象。
类图如下,注意红色标注的地方:
拉的方式能比较好的让原本被动的观察者也有了相对的主动。
进一步思考-谁是谁的观察者
由于电气设计的需要,我们的上位机软件常常需要控制多台下位机,比如我曾经参与过的一个客户是索尼的项目,一个上位机同时控制18个下位机(单片机)。我们站在上位机的角度,上位机是观察者,下位机是主题,相当于一个上位机同时观察者多个主题;
换个角度,站在下位机的视角上看,下位机在等待上位机的通知,一旦得到上位机的通知则立刻执行相应操作,这时下位机是否可以看成观察者,上位机是主题呢?
在实际的业务中,特别是交互型的系统,这种现象很常见,那么谁是谁的观察者呢?
我想大家和我一样,应该可以找到答案:谁是谁的观察者并不重要,“推”和“拉”哪个更好也没有定论,在适合的地方采用适合的方案,这种抉择才是最重要的。
进一步思考-允不允许相互引用?
在以“拉”方式实现观察者模式的设计中,主题可以直接调用观察者的方法(在主题的NotifyObservers方法中直接调用了观察者的Update方法),观察者也能直接调用主题的方法(在观察者中“拉”数据的时候调用了主题的GetData方法),这种相互引用是否合理?是否会造成维护的困难?是否会造成一些安全上的隐患?
这个,我暂时也没有答案。
最后
观察者模式的总结到这里就结束了,思考还没有结束。 昨天下午在参加公司年度技术峰会的时候,一位同事在演讲的时候,提问大家模式解决了什么问题?很荣幸我的回答得到了他的认可,奖品是一个小的布老虎。
模式解决的问题是变化。
在最后温习一下OO设计的一个原则:
为解除软件之间的紧耦合而努力。