基于BREW平台松耦合设计IV-重构AlarmService的整体设计

来源:互联网 发布:linux 扩大磁盘空间 编辑:程序博客网 时间:2024/05/12 04:54

 

基于BREW平台松耦合设计IV-重构AlarmService的整体设计

毛晓冬 2008-5-6

 

一、          概述:

本文主要通过一个实际模块的改善过程,感性的体现设计原则的使用。

 

二、          目前的AlarmService框架:

 

目前的AlarmService主要包括两个模块,AlarmAppTodoAppAlarmApp主要负责闹铃(包括自动关机,关机闹铃)功能,TODO主要负责类似日程提醒的功能。随着需求的增多,目前这两个模块其实比较复杂。目前的整体设计框架如下:

 

 

 

 

AlarmApp负责UI的显示,Alarm的设置,过期处理,计算下一个Alarm,存取Alarm的持久性数据。

TODOAlarmApp类似。这里可以看到,AlarmAppTODOApp模块内部直接完成了所有的逻辑。

我们再看看模块间的交互(按上图的序号):

1.  日程提醒到期后,TODOApp通过发送EventEventBox,由EventBox来弹出日程提醒框。

2.  SettingApp中可以设置自动关机闹铃,SettingApp通过发送EventAlarmApp,由AlarmApp来最终设置该自动关机的闹铃,使得闹铃的控制完全由Alarm负责。

3.  闹铃到期后,AlarmApp通过发送EventEventBox,由EventBox来弹出闹铃提醒。在EventBox界面,当用户选择Snooze或者关闭时,EventBox再发送Event通知AlarmApp应该Snooze还是关闭。

4.  AlarmApp中不存在Active闹铃时,AlarmApp会发送EventTODOApp询问是否TODOApp也不存在Active的日程。TODOApp通过回Event的方式告知结果。如果TODOApp也无Active日程时,AlarmApp发送EventCoreApp通知清除系统栏的闹铃图标。 TODOApp中不存在Active日程时,处理类似。

5.  AlarmApp使用BREWAlarmService提供的API。同时,BREWEvent方式通知AlarmApp闹铃的过期。

6.  TODOApp通知CoreApp清除闹铃系统栏(见4

7.  TODOApp使用BREWAlarmService提供的API。同时,BREWEvent方式通知TODOApp日程的过期。

8.  AlarmApp通知CoreApp清除闹铃系统栏(见4

 

三、          目前框架的缺点:

1.  紧耦合:这里存在两个紧耦合,一是模块内的紧耦合,一是模块间的紧耦合。

a.  模块内的紧耦合:AlarmApp/TODOApp应用代码内部完成了包括UI显示,设置Alarm,日程,处理过期,存储数据等等的所有任务。整个显得较乱。导致某一方面的变化(更改,比如UI的)影响到整个应用,不得不更改整个应用。牵一发而动全身,稍有不慎,无关联的方面也会受牵连,出现问题。不仅如此,模块内的紧耦合还导致代码逻辑混乱,不利于维护,更加不利于新人的阅读和掌握。

b.  模块间的紧耦合: 如上面的框图,可见多个模块间直接使用Event方式通信,在编译期就绑定在了一起,呈现出网状(多 多)的耦合。这种模块间的紧耦合,导致单个模块的变化可能不期望的影响到其他模块,并且也使得代码逻辑混乱,不利于维护,也不利于新人的阅读和掌握。

2.  低复用:其实TODOAppAlarmApp两个模块除了UI的差别比较大外,内部的逻辑处理(如何设置alarm,如何处理Alarm到期等等)以及持久性数据的存储都基本一样,完全可以复用。但目前的方案下,完全由两个模块重复的独立自己实现一套。

3.  不易扩展:扩展性是和耦合性密切相关的,耦合的如此紧密,那基本上就只能是“修改”,而不是扩展了。

4.  难以抵抗变化:需求往往带动变化,永恒不变的就是“变化”。所以,一个设计的好坏,就是看它是否能抵抗可预见的变化。变化只有被各自独立的封装起来后,才有可能抵抗变化,因为此时一个变化点不会影响到另一个不变点。而目前的设计,模块间和模块内的紧密耦合,根本无法抵抗变化。UI的需求变化,导致直接更改整个App,业务逻辑的变化,也导致更改整个App

 

四、          如何逐一改善:

1.  利用三层结构解决模块内的紧耦合:

从上面的分析可以看到,模块间的耦合主要是由于把所有逻辑都混在一起了,没有分层处理。当进行分层处理后,就可以区分不同的逻辑,进而就可以封装并隔离变化点,让每一部分的变化独立的进行。

以抽象的观点来看AlarmApp或者TODOApp模块,存在3个独立的变化点,UI,内部逻辑,数据存取。由此,我们可以将其分解为UI逻辑,业务逻辑,数据访问三层,或言之3个变化点。分解之后,就是封装,封装之后,各个变化点的变化就不再会影响其他部分了。

这里UI逻辑以应用形式实现,即App只负责UI

业务逻辑以扩展接口独立,IAlarmEx,负责AlarmService相关的业务逻辑,内部访问数据访问接口,对外提供:获取Alarm状态,设置AlarmAlarm过期后的自动处理,Snooze处理等等

                     数据访问以接口独立(目前使用IDataBase)负责持久性数据的存取。

 

2.  利用中介者(Mediator)方式解决模块间的紧耦合:

原有的模块间都是直接交互(事件方式),体现出网状。这是紧耦合的根源。

可以试着引入中央控制层,所有模块都只和中央控制层交互,由中央控制层“代劳”与目的模块交互。从而,将多 多的关系, 简化为 一的关系, 便于控制。 这种降低多 多之间的耦合方式, 其实就是 中介者(Mediator 方式。

当将IAlarmEx设计成一个公共接口提供AlarmService的业务逻辑时, IAlarmEx就是一个中介者。 所有的模块,现在都只和IAlarmEx接触,IAlarmEx内部直接消化相应的业务逻辑处理,除非必要时,IAlarmEx才会和其他目的模块交互。

 

3.  解决低复用:

由于IAlarmEx是一个公共的业务逻辑接口,TODO Alarm基本只是UI的差异,所以, TODO可以完全复用IALARMEx接口。

 

4.  解决可扩展:

由于分层,封装了变化点,使得扩展性增强了,即,在不改变原有整体代码的基础上,扩展功能。

 

5.  数据的透明传输:

原本的某些模块间传递特殊的App-Spec 数据,比如AlarmApp需要将当前到期的AlarmID,铃声文件路径等信息传递给EventBoxTODOApp则必须把日程内容传递给EventBox。这些数据,都属于UI逻辑范畴,进一步讲,属于通信的端点的范畴。必须对中介者,或者说业务逻辑而言是透明的,否则,随着通信端点Spec的变化,或者UI逻辑的变化,就会使得中介者,或者业务逻辑也产生不期望的变化。为了隔离这种变化,我们引入了数据的透明传输,即IAlarmEx接口提供设置Alarm,或者Add一个日程时,API的最后一个参数为VOID*的透明数据,IAlarmEx只负责存储该透明数据,并当Trigger时,把该透明数据传递给Destination,比如EventBox,至于究竟是什么数据,一无所知,在通信的发,收方,完成数据的封装和提取,其实就是一个VOID*向特殊指针的转换。其实,这和OSI7层协议很类似,底层接口向上层接口服务,并且完成数据的透明传输。

 

6.  使用Adaptor

我们分层后的设计,其实和平台是无关的。但是,因为我们运行在具体的平台上,所以往往可能出现平台所期望的接口和我们设计的接口不一致,此时我们就需要一个Adaptor

在我们的新的设计方案中,一切业务逻辑都是在IAlarmEx中完成,包含Alarm到期后的处理,完全不需要和其他模块相关联。但是,BREW平台的AlarmService必须存在一个App的载体,因为其设置Alarm时,必须指定AppCLSID,即,Alarm是为App设置的,而不是接口。同样的,当Alarm到期后,BREW通知的是App,而不是接口。

 

不过不要紧,这种差异性,我们只需要引入一个Adaptor即可。基于BREW的限制,该Adaptor必须是一个App。因为,我们同时设计了一个AlarmDaemon App,主要就是完成Adaptor的功能,即,IAlarmEx的实现中,当设置Alarm时,使用AlarmDaemonCLSID(将其作为载体)。Alarm过期后,AlarmDaemon App收到通知,此时其处理非常简单,直接调用IAlarmEx_Trigger API即可。通过AlarmDaemonAdaptor功能,使得其他任何模块,都没有被关联进不该关联的业务逻辑中来。

 

 

五、          重构后的新框架:

 

 

 

基于前面的描述和上面的框图,大家应该对重构后的新方案比较熟悉了。这里再说明一下,基于重构后的新方案,原有的场景,现在变得多么简单(按序号):

1.  日程提醒或者闹铃到期后,IAlarmEx_Trigger被调用,内部计算下一个闹铃并设置后,就会自动的通知EventBox,并传递相应的透明数据。EventBox弹出提示框。当用户选择SNOOZE时,EventBox调用IAlarmEx_Snooze即完成了Snooze闹铃的设置。

2.  SettingApp通过调用IAlarmEx_SetAlarm来设置自动关机闹铃,同时可以调用IAlarmEx_SetSnoozeMode来更改Snooze ModeValue

3.  AlarmApp通过调用IAlarmEx_GetXXX来获取Alarm数据并在UI上显示,当用户设置,取消Alarm时,调用IAlarmEx_Set/CancelAlarm

4.  TODOAlarmApp类似。

5.  IAlarmEx内部使用BREWAlarmServiceAPI

6.  对于自动关机闹铃到期后,IAlarmEx内部通知CoreApp并传递透明数据,CoreApp实现关机。此外,当IAlarmEx监测到没有任何Active TODO Alarm存在时,则会通知CoreApp去除系统栏的Alarm标志。

7.  AlarmDaemon作为Adaptor App充当载体,接收BREW AlarmService的通知,并委托给IAlarmEx处理。

8.  Alarm到期后BREWAlarmService通知AlarmDaemonApp

 

需要注意的是,框架中的IAlarmEx主动和其他模块间的交互,是通过事件的方式。但却不是在编译期绑定的,即没有使用硬编码的CLSID,而是使用了BREWMIME Handler机制,在运行时动态的绑定,从而解除了通信时的耦合。

 

六、          如何抵抗需求的变化:

逻辑被分离了,变化点被封装了。从而可以抵抗需求的变化。比如:

新项目对UI的要求较高,此时只需要更改UI逻辑即可,完全不会影响到业务逻辑。

新项目要求支持关机闹铃,并且支持时间可设置,此时只需亚更改IAlarmEx,即业务逻辑,完全不影响应用。

新项目要求EventBox显示闹铃时更加个性化,显示的风格可以定制。此时,只需要更改AlarmAppEventBox的“通信协议”,由于IAlarmEx使用了数据透明传输,所以此时并不影响IAlarmEx,即不影响业务逻辑。

新项目要求使用新员工接手该模块,不用怕,新员工2天内就理顺了代码 J

 

七、          总结:

本文通过对现有的AlarmService的框架提出缺点,并且讲解如何一步步解决问题,最终重构得到一个新的框架,感性的摄入了一些重要的软件设计思想,希望对大家有帮助。

注意:本文仅从理论上描述了一个重构的方案,至于新方案是否会在新项目或者新平台中使用,这个和具体的实施进度,项目进度有关。目前来看,由于前期的实现,验证,移植尚未完全完成,所以在6055上使用的可能性也并非100%。

 

 


原创粉丝点击