基于BREW的松耦合设计初探
毛晓冬 2007-10-19
一、 概述:
在进行模块/系统设计时,我们通常应该遵循“低耦合、高内聚”,“针对接口编程,不针对实现编程”等原则,这使得我们的设计可以被重用同时易于扩展和维护,可以抵抗“需求”的不断变化。BREW中,大量使用应用和扩展来实现各种功能,如果处理不当,会使得大量的这些对象耦合起来,整个系统将很难抵抗“变化”。本文就BREW系统中如何进行松耦合的设计进行初步的探究。
二、 Browser需求:
本文以Browser这样一个实际的模块作为例子来讲述各种方案的演变。我们假设Browser模块对外提供的通用的功能为:能够以默认方式启动(进Browser的主菜单界面),能够启动并直接加载主页,能够启动并直接加载客户指定的URL,能够启动并直接进入互动信箱,能够清空互动信箱,能够恢复出厂设置。
三、 Browser App处理一切的方案
我们的第一个方案是最“传统”的,一切都由Browser App来处理。方案框图如下:
在这种方案下,你可以看到一切都是“外界”直接和Browser App交互。在BREW中,最典型的实现是,其他应用都通过事件来得到Browser的服务,如上,App1通过事件的方式请求Browser启动,加载URL或者进入互动信箱。App2也利用事件的方式请求Browser模块清空互动信箱和恢复出厂设置,这里的双向箭头代表的是,可能App2需要得到一个操作的结果,那么Browser App还需要通过事件告知这个“结果”。
一切都是直接交互。
一切都是针对实现。
App1,App2和Browser紧紧的耦合在一起。相互之间必须知道对方的存在(CLSID)。
现在,我们来看看当需求变化后发生了什么。
假设某一天,市场部认为现在的Browser界面太难看,可操作性太差,所以“枪毙”了现有的这个应用提供商,他们找了另外一个有丰富经验的Browser开发商来替换这个应用。
针对上面的框图,也就是“变化”发生了。我们可以看到,当变化发生时,虽然变化的仅仅是Browser,但是我们的App1,App2不得不将原先与Old Browser的交互转而延伸到与NewBrowser的交互。
牵一发而动全身!
为什么会这样??
因为我们针对了实现编程。
我们没有将变化点封装起来。
四、 分离出Browser Extension的方案
对于上面的方案,有一点需要注意的是,清空互动信箱和恢复出厂设置可能仅仅是一种数据处理,或者说是逻辑处理,完全没有必要启动应用。启动应用会带来额外的开销。此时,我们可以将职责分离,将Browser模块的逻辑/数据操作单独封装成一个对象(对应BREW中就是Extension),Browser App本身采用组合的方式引用该对象来完成具体的逻辑/数据处理,而自身仅仅完成高层的UI处理。同时,Browser模块也利用该封装的对象对外提供一致的逻辑/数据处理。使得客户如果仅仅需要请求逻辑/数据服务时,无须将Browser App牵扯进来,去除不必要的开销。
更改后的方案如下面框图所示。
不过该方案的目的在于分离职责,将逻辑与UI分离,使得各自的变化不影响对方。其本质是使得Browser模块中的逻辑和UI解耦。
但是,该方案仍然没有解决客户和Browser模块间的耦合。因为客户仍然面对直接和“实现(CLSID)”的交互。所以一旦“需求”变化后,客户不得不再次面临“灾难”。
五、 使用MIME Type降低耦合的方案:
现在是时候该让客户(App1,App2)和实现(Browser模块)进行解耦了。基本的原则,仍然是必须面向接口编程,而不是面向实现。封装各自的变化,并在运行时才绑定双方,而不是编译时!对于BREW,MIME Type Handler是一个现成的很好的可被利用的机制。利用该机制,可以轻松的实现上面的“奢求”。我们先看一下方案框图吧:
利用MIME Type Handler机制,可以将一个对象(实现)与一个MIME Type绑定(注册)。而客户对于具体对象(实现)的请求,现在改为向MIME Type Manager的请求,委托MIME Type Manager来与真正的对象(实现)交互,或者委托MIME Type Manager返回一个运行时的具体(实现)对象给客户的抽象接口。 BREW的MIME Type机制,就好像是一堵墙,隔离了客户和具体的实现对象,只要事先遵循一组事先约定的规约(接口,这里就是MIME Type),双方就可以单独的变化,而不影响另一方。甚至双方根本不知道对方的存在。
好了,说了这么多的大道理,我们来看看针对Browser,具体如何实现吧。
1.将BrowserApp的对外服务抽象成一个MIME Type,假设为WapService,并且约定下面的规约:
WapService://DefaultLaunch 默认方式启动
WapService://MailLaunch 启动并进入互动信箱
WapService://HomePageLaunch 启动并加载主页
WapService://URLLaunch?URLValue 启动并加载客户指定的URL
2.BrowserApp在MIF中注册该MIME Type,即 WapService
3.BrowserApp在自己的实现中履行该规约,即,在HandleEvent中实现上述请求。
4.Browser Extension继承至一个抽象接口,IBrowserBase,并实现所有的方法。
5.Browser Extension注册自己的MIME Type,假设为WapServiceExtension。
6.Browser模块无须将任何信息暴露给客户。对于上面提到的规约和IBrowserBase.h,那甚至应该是客户提供的!
7.客户需要请求Browser App提供服务时,只需要调用ISHELL_SendURL/PostURL并按照规约传入不同的请求MIME Type String即可。注意,这里客户对“实现”一无所知,这种绑定是在运行期,且委托给了BREW内核。CLSID这个该死的家伙现在已经不需要了!
8.客户需要请求Browser Extension提供服务时,只需要调用ISHELL_GetHandler并按照规约传入WapServiceExtension,即可获得具体Extension的CLSID并进行创建接口。需要注意的是,这里获得的CLSID是在运行时动态获得的(BREW内核返回的),并非在编译期绑定。所以具体对象的实例化是在运行时才确定的,唯一只有抽象接口才是在编译期决定的,但是这又何妨,这本身就是一种规约。
9.客户获得实例化的Extension对象后,就可以按照规约(IBrowserBase抽象接口定义的抽象方法)来使用它了。
现在,我们来看看当“需求”变化时的情况:
很不幸,现有的Browser提供商终究还是被“枪毙”了,新的Browser供应商来到了。在最早的时刻,我们就把我们的规约扔给了他们,包括IBrowserBase.h以及其他的MIME Type规约。(他们为我们提供服务,所以得听我们的,规约由我们来定J)
OK,新的供应商很有经验,没几天就按规约完成了一切。此时,我们需要集成它们的模块,我们需要作什么??
我们不需要修改任何客户代码!
我们仅仅需要重新编译,连接生成我们的Image文件(如果以静态方式集成的话)。
我们甚至连Image文件都不需要重新生成(如果直接加载动态应用的话)。
这一切是如何发生的那??
请重新看看上面框图中的那堵墙,那些规约。
别高兴的太早,供应商也从我们这里学到了。现在它们需要我们的服务了,它们也扔了一份规约给我们。
该我们行动了:-)