也谈代码隔离

VBA支持一直是我们发布的组件的内建功能,目的有两个:一是支持内部自动化测试,二是提供给用户宏扩展的能力。但由于我们提供的是一个组件,而用我们这个组件的软件不一定需要VBA的功能,事实上,由于VBA的日渐没落(微软早就停止升级VBA了,这就是为什么VBA没有64位版本的原因,也预示着VBA的必将最终消亡),大家基本都不想要了。然而,在我们库中,VBA并不是一个可选的组件,你用也好,不用也罢,VBA的license费还是要付的。

为了避免客户不必要的费用,我们应该拿掉VBA;但为了内部测试的支持,我们又必须保留VBA。这就意味着我们必须保留VBA的代码,并把其很干净的隔离开来。

但是,由于当初设计的时候,并不是将VBA作为一个可选功能来考虑的,所以内部VBA相关的函数调用到处都是。根据不同情况总结一下是怎么做的吧。

首先,我们当然会提供一个函数用来判断是否运行于客户的进程中:

bool IsClient();

抽象工厂 (Abstract Factory)

对VBA API的调用是通过我们自己的wrapper的:

image

 

这里IVbaHandler中是无数个VBA相关的纯虚函数。分别对应有32位和64位的实现,这是因为VBA组件并不支持64位,所以在64位系统下,我们需要通过另起一个32位的Host Process来实现VBA的支持。CVbaHandlerFactory会根据当前的环境返回正确的对象。

然后,我们代码中是无数个基于IVbahandler的调用。

如何隔离?如果此时你想到的是在CVbaHandler32和CVbaHandler64的每个函数的实现中最前面加一个IsClient的判断,如果成立直接返回,你可能会觉得自己很傻;但是别担心,还有更傻的,那就是在所有调用到IVbahandler地方加IsClient的判断,如果是成立,则不调用。

这样的做法缺点很多,列几个吧:

  1. 这样做很痛苦。工作量大,且都是重复劳动,容易漏掉点什么。
  2. 代码巨难看,可维护性大大下降 - 恭喜,你为后来人又提高了一级门槛。
  3. 万一将来要重新启用这个功能~~~请参见1,2条

虽然道理大家都懂,我们还是经常有机会在代码中看到无数个if在飞的的情况。

 

其实,在现有代码的基础上,你很容易想到这个方案:

image

 

新派生一个 CVbaHandlerDummy的类,正如其名字表示的,它什么都不做,只是对IVbaHandler的一个空的实现,CreateVbaHandler()会在IsClient()成立的时候返回这个对象。如此,我们对原有代码所作的改变只是:

IVbaHandler* CVbaHandlerFactory::CreateVbaHandler()
{
if(IsClient())
return new CVbaHandlerDummy();
else
{
#ifdef _WIN64
return new CVbaHandler64();
#else
return new CVbaHandler32();
}
}

引入中间层

我们的文档类是从CApcDocument派生过来的(Apc是对微软对VBA的一层C++封装),如下

CMyDocument -+ CApcDocument -+ COleServerDoc

这里,在CMyDocument中会调用到CApcDocument的一些函数:

BOOL CMyDocument::OnNewDocument()
{
CApcDocument::OnNewDocument();
//...
}

如何绕过对Apc的调用?我们可以通过判断解决:

BOOL CMyDocument::OnNewDocument()
{
if(IsClient())
COleServerDoc::OnNewDocument();
else
CApcDocument::OnNewDocument();
}

即使这个方案是可行的,这里也严重污染了CMyDocument类,看到满天在飞的if语句了吗~~~

而且事实上,在遇到虚函数时,这中方法并不能解决。相信大家都熟悉模板方法(template method)这个模式,当在CApcDocument的中override了一个会在其基类的模板方法中调用到的虚函数时,上面的方法就行不通了:因为该调用根本就不会出现在CMyDocument的实现中:

BOOL COleDocument::OnOpenDocument(LPCTSTR lpszPathName)
{
//...
LoadFromStorage();
//...
}

OnOpenDocument这个函数会被MFC的framework所调用,因为CApcDocument重载了LoadFromStorage,该函数自然也会被调用到,而这是我们不希望看到的。如何解决?要知道我们并不能修改CApcDocument的实现。

我的方案是在CMyDocument和CApcDocument之间引入一个中间层:

CMyDocument -+ CApcDocumentProxy -+ CApcDocument

然后将在CApcDocument中被override的虚函数,全部都在CApcDocumentProxy中override一遍,以跳过CApcDocument,实现转发:

void CApcDocumentProxy::LoadFromStorage()
{
if (IsClient())
return COleServerDoc::LoadFromStorage();
else
return CApcDocument::LoadFromStorage();
}

另一个好处是我们把所有的判断都集中到了CApcDocumentProxy这个类中了。

 

封装,而不是显式判断

另外,还发现的一些地方,我们可能会倾向于直接用IsClient来判断是否需要调用:

  1. 直接调用了VBA的API,并未通过我们的VbaHandler类。
  2. 是通过了VbaHandler类来调用,但还有后续操作需要分是否IsClient处理

但比较规范的做法是将这样的调用封装到VbaHandler中去,对,第二种情况说明你在VbaHandler中封装的粒度还可以再大点。避免扩散的IsClient的调用,从而保持代码的简洁与一致。

这里的一些做法,保证了只在一个统一的构造中集中进行隔离(CVbaHandlerFactory, CApcDocumentProxy),应该算是比较干净的了。而且将来如果高层哪根脑子抽筋,将其换回来也是相当的简单与安全的。

posted @ 2010-02-25 16:59  lzprgmr  阅读(1750)  评论(0编辑  收藏  举报

黄将军