zoukankan      html  css  js  c++  java
  • 也谈代码隔离

    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),应该算是比较干净的了。而且将来如果高层哪根脑子抽筋,将其换回来也是相当的简单与安全的。

  • 相关阅读:
    矩阵距离
    CF409D Big Data
    AT2685 i18n
    P3366 【模板】最小生成树
    P3367 【模板】并查集
    ISBN(洛谷P1055)
    关于数组
    0021---一元一次方程
    0020---求圆锥体积
    0019---求圆台的体积
  • 原文地址:https://www.cnblogs.com/baiyanhuang/p/1730712.html
Copyright © 2011-2022 走看看