新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> It is the theory that decides what can be observed. - Albert Einstein
    [返回] 中文XML论坛 - 专业的XML技术讨论区计算机理论与工程『 理论计算机科学 』 → 乌托邦式的接口和实现分离技术 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 9874 个阅读者浏览上一篇主题  刷新本主题   平板显示贴子 浏览下一篇主题
     * 贴子主题: 乌托邦式的接口和实现分离技术 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 理论计算机科学 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客楼主
    发贴心情 乌托邦式的接口和实现分离技术

    《Imperfect C++》中展示了一种叫“螺栓”的技术,然而,这本书中的讨论并不足够深入。当然,我也相信Matthew是故意的,从而让我们这些“三道贩子”(Matthew自称是二道贩子)也能够获得一点点成就感。
    非典型秃子http://blog.csdn.net/wingfiring/


    --------------------------------------------------------------------------------

      考虑这样一个接口设计:
        struct IRefCount;
        struct IReader : public IRefCount;
    在Reader中实现接口:
    <!--[if !supportEmptyParas]-->    class Reader : public IReader;

      在上述的继承结构中,IRefCount是一个结构性的类,用来实现引用计数,实际上它和领域逻辑部分IReader没有什么关系。我们打算在IRefCount的基础上,建立了一套工具来管理对象生命周期和帮助实现异常安全的代码 (例如,smart pointer) 。现在来考虑Reader的实现,Reader除了需要实现IReader的接口,还必须实现IRefCount的接口。这一切看起来似乎顺理成章,让我们继续看下面的设计<!--[if !supportEmptyParas]-->:
        struct IWriter : public IRefCount;
    <!--[if !supportEmptyParas]-->    class Writer : public IWriter;

      现在来考虑Writer的实现,和Reader一样,Writer除了要实现IWriter的接口外,同时还需要实现IRefCount的接口。现在,我们来看看IRefCount是如何定义的:
        struct IRefCount {
            virtual void add() = 0;
            virtual void release() = 0;
            virtual int count() const = 0;
            virtual void dispose() = 0;
            virtual ~IRefCount(){}
        };
    在Reader中的IRefCount的实现:
        virtual void add() { ++m_ref_count;}
        virtual void release() {--m_ref_count;}
        virtual int count() const{return m_ref_count;}
        virtual void dispose() { delete this;}
        …
        int m_ref_count;

      同样,在Writer的实现中,也包含了一模一样的代码,这违背了DRY原则(Don’t Repeat Yourself)。况且,随着系统中的类增加,大家都意识到,需要将这部分代码复用。一个能够工作的做法是把IRefCount的实现代码直接放到IRefCount中去实现,通过继承,派生类就不必再次实现IRefCount了。我们来看一下dispose的实现:
        virtual void dispose() { delete this;}
    这里,采用了delete来销毁对象,这就意味着Reader必须在堆上分配,才可能透过IRefCount正确管理对象的生命周期,没关系,我们还可以override dispose方法,在Reader如下实现dispose:
        virtual void dispose() { }


      但是,这样又带来一个问题,Reader不能被分配在堆上了!如果你够狠,当然,你也可以这么解决问题:
        class HeapReader : IReader;
        class StackReader : HeapReader{ virtual void dispose() { } };
      


      问题是,StackReader 是一个HeapReader吗?为了代码复用,我们完全不管什么概念了。当然,如果你和我一样,看重维护概念,那么这么实现吧:
        class HeapReader : IReader;
        class StackReader : IReader;

      这样一来,IReader的实现将被重复,又违背了DRY原则,等着被将来维护的工程师诅咒吧!或许,那个维护工程师就是3个月后的你自己。如果这样真的能够解决问题,那么也还是可以接受的,很快,我们有了一个新的接口:
        struct IRWiter : IReader, IWriter;
        class RWiter : public IRWiter;

      考虑一下IRefCount的语义:它用来记录对所在对象的引用计数。很显然,我从IReader和IWriter中的任意一个分支获得的IRefCount应该都是获得一样的引用计数效果。但是现在,这个继承树存在两个IRefCount的实例,我们不得不在RWiter当中重新重载一遍。这样,从IReader和IWriter继承来的两个实例就作废了,而且,我们可能还浪费了8个字节。为了解决这个问题,我们还可以在另一条危险的道路上继续前进,那就是虚拟继承:
        struct IReader : virtual public IRefCount;
        struct IWriter :  virtual public IRefCount;

      还记得大师们给予的忠告吗--“不要在虚基类中存放数据成员”。“这样有什么问题吗,我们不必对大师盲目崇拜”,你一定也听过这样的建议。如果大师们不能说服这些人,那么我也不能。于是,我们进一步在所有的接口中提供默认实现,包括IReader和IWriter.

      现在的问题是:
        struct IRWiter : IReader, IWriter;
    还是
        struct IRWiter : virtual IReader, virtual IWriter ?

      如果你没有选择virtual,那么IRWiter被派生后,那么派生类的继承树中可能存在多个IReader实现,如果这个派生类要求只能提供一份IReader的语义怎么办?除了重新实现接口还能怎样?反过来,如果我们选择了virtual继承,那么派生类需要多个实现怎么办?真是个麻烦事。“这是典型的过度设计,我们为什么要考虑这么多?”你可以这么说,但事实上,即使是一个数百文件的小型系统,也完全可能迫使你作出选择。虽然,我们仍然有办法作出挽救措施,但是也只是苟延残喘而已。正如我前面所说,这是一个危险的道路,聪明如你,是断然不会让自己陷入这样的泥潭的。

      让我们离开虚拟继承,先回到重复代码的问题上来。有没有更好的解决办法呢?还好,在C++的世界里,我们有神奇的template,让我们来消除重复的代码:
        template<typename Base>
        class ImpReader : public Base{
            constraint(is_base_derive(IReader, Base))
            Implementation IReader
    <!--[if !supportEmptyParas]-->    };
        class HeapReader : ImpReader<IReader>{};
        class StackReader : ImpReader <IReader>{
              virtual void dispose() {};
    <!--[if !supportEmptyParas]-->    };

      请注意,我们还是假设IRefCount已经提供了一个默认实现。现在,情况好了很多,所有的代码都只有一份,而且,概念也没有被破坏。假设,Writer也同样需要类似的能力,那么,我们又多了StackWriter和HeapWriter.事实上,真的有人用到了StackWriter吗?我不知道,只是,提供了StackReader,没有理由不提供StackWriter啊。让我们继续。

      现在,我们发现,需要改进内存分配的性能问题,于是,我们希望通过内存池来分配对象,相应的dispose也需要修改:
        virtual void dispose(){ distory(this);}
    于是,我们又多出两个类,PoolReader和PoolWriter。这真是糟糕,组合爆炸可不是什么好兆头。

      从我们前述的变化来看,都是IRefCount在变化,为什么不把这种变化分离出来呢?不必为IRefCount提供默认实现,借鉴ImpReader的手法:
        template<typename Base>
        class ImpHeapRefCount : public Base{
            constraint(is_base_derive(IRefCount, Base));
        ..};

    类似的:
        template<typename Base> class ImpStackRefCount : public Base;
    <!--[if !supportEmptyParas]-->    template<typename Base> class ImpPoolRefCount : public Base; <!--[endif]-->

    再看看,我们如何实现所有的Reader.
        typedef ImpReader< ImpHeapRefCount<IReader> > HeapReader;
        typedef ImpReader< ImpStackRefCount<IReader> > StackReader;
        typedef ImpReader< ImpPoolRefCount<IReader> > PoolReader;
    以HeapReader为例,实际的继承关系是这样的:
        ImpReader-->ImpHeapRefCount-->IReader-->IRefCount;

      对于Writer,我们完全可以采取同样的手法来实现。对于上述的typedef可以预先定义,也完全可以不定义,交给最终用户去组装吧。现在,类的设计者再也不必为选择实现而痛苦了,你只要提供不同的砖头,客户程序员可以轻而易举的建立起大厦。还有比这更让一个设计师幸福的吗?

      继续深入,考察ImpHeapRefCount和ImpStackRefCount的实现,我们提到,dispose方法的实现是不一样的,但是,其他部分:add,releasee和count的实现完全可以相同。然而我们现在又分别实现了一遍,为了不违背DRY原则,我们如下处理:
        template<typename Base>
        class ImpPartialRefCount : public Base{
            //实现add, release和count.
        };

        template<typename Base>
        class ImpHeapRefCount : public Base{
            virtual void dispose() { delete this;}
        };

        template<typename Base>
        class ImpStackRefCount : public Base{
            virtual void dispose() { }
        };
    然后,我们可以这样定义Reader:
        typedef ImpReader<ImpHeapRefCount<ImpPartialRefCount<Ireader> > > HeapReader;

      请注意,我们在这里展示了一种能力,不必在一个实现当中完整的实现整个接口,可以把一个接口的实现分拆到多个实现当中。这个能力是非凡的,借助于此,我们可以提供更小粒度的实现单位,给最终用户来组装。具体拆分到什么样的程度完全取决于代码复用的需求,以及概念维护的需要。

      我们提供了高度的复用能力,同时避免了继承带来的强耦合,以及对推迟设计决策的支持,这些能力对于软件设计师而言,正如Matthew在《Imperfect C++》中所说的,这简直就是现实中的乌托邦!

      现在我们把这种手法首先针对单继承做一个小结。对于任意的接口IInterface,我们提供如下的实现:
        template<typename Base>
        class ImpInterface : public Base{
            constraint(is_base_derive(IInterface, Base));
        };
    请注意,一个接口可以有任意多个实现,并且可以是任意的部分实现。<!--[if !supportEmptyParas]--> <!--[endif]-->

      假设我们有如下接口继承树:
    InterfaceN -->InterfaceN_1-->InterfaceN_2-->…-->Interface0
    并且提供了实现类ImpInterface0 ~ ImpInterfaceN.
    那么,InterfaceN的实例类型就是:
        typedef ImpInterfaceN<
                ImpInterfaceN_1<
                ImpInterfaceN_2<
                …
                ImpInterface0<InterfaceN> …> > >  ConcreteClassN;
    我们注意到,定义ConcreteClassN的时候,我们的ImpInterface是按照顺序来的,我认为这是合适的做法。当然了,最后组装的权力已经交给客户了,客户爱怎么组装就怎么组装吧。然而我们还是有一些需要注意的问题。
        1.假定,我需要在ImpInterfaceI中引用基类的方法,记住,不要使用这样的手法:
        ImpInterfaceI_K::SomeMethod();
    这样调用不具有多态性,而应该这样:
        this-> SomeMethod();
        2.不要在自己的ImpInterfaceI实现中覆盖基类接口的其他已经实现的方法,如果你一定要这么做,那么务必在文档中说明,因为在组装的时候,顺序将是极其关键的了。
        3.这个方法和设计模式中的Template Pattern目的是不一样的。Template Pattern是在基类中定义了一个算法,让派生类定制算法的某些步骤。这里的方法针对的是接口模型的概念,提供接口和实现分离的技术。

      关于第二条,应该尽量避免发生。这里说的覆盖是指基类实现已经实现了该方法,而后继实现又覆盖该方法。基类实现可以是一个部分实现,对于没有实现的那些方法,在派生接口的实现类中实现则是常见的。一方面,我们尽量合理分解层次之间的功能,另一个方面,可以通过定制实现模板类,来保证顺序。尽可能的让语言本身来保证正确性,而不是依赖文档。我们可以像这样预先装配一些东西:
        template<typename Base>
        class SomeComponent : public ImpPartA < ImpPartB <Base> >{};
    可惜,C++暂时还不支持模板的不完全typedef,否则,我们还可以如下定以:
        template<typename Base>
        typedef ImpPartA< ImpPartB<Base> > SomeComponent;
    不过,C0x很可能会支持类似的语法。这样,我们使用SomeComponent来作为一个预制品,来保证一些安全性,而不必完全依赖文档了。 <!--[endif]-->

      看看ConcreteClassN的定义,也许你和我一样,并不喜欢这种嵌套的、递归的定义方式,太难看了。让世界稍微美好一点吧!于是我们提供一个辅助类:
    <!--[if !supportEmptyParas]-->    template<typename T>struct Empty{};

        template<typename I, typename template<class> class B>
        struct Merge{       <!--[if !supportEmptyParas]-->typedef B<I> type;};

        template<typename I >
        struct Merge<I, Empty >{
           typedef  I type;
    <!--[if !supportEmptyParas]-->    }; <!--[endif]-->

        template
        <
            typename I,
            typename template<class> class B1,
            typename template<class> class B2 = Empty,
            …
            typename template<class> class Bn = Empty,
        >
        struct Reform{
            typedef   typename Merge<
            typename Merge<
            typename Merge<I, B1>::type
                   , B2>::type , …,Bn>::type type;
        };
    现在,我们可以这样定义ConcreteClassN了:
        Typedef Reform<InterfaceN, ImpInterface0, ImpInterface1,
        …ImpInterfaceN>::type ConcreteClassN;
    是不是清爽了很多?
    在继续下面内容以前,请回味一下这个不是问题的问题:
      假设IReader有3种实现,IRefCount有3种实现,我们将如何漂亮地解决掉他们。 <!--[endif]-->

      现实世界总是要复杂得多,让我们进入真实的世界。回顾这个接口:
        struct IRWiter : IReader, IWriter;
    假设我们确实需要IReader, IWriter,但是并不需要IRWrite,可不可以让一个对象同时支持这两个接口呢,就像COM一样?当然可以,我们借助于这样一个辅助模版:
        template<typename B1, typename B2>
        struct Combine : B1, B2{
            typedef B1 type1;
            typedef B1 type2;
    <!--[if !supportEmptyParas]-->    }; <!--[endif]-->

        typedef Reform< Combine<IReader, IWriter>, ImpRefCount,  ImpWriter,  ImpReader >::type ConcreteRWiter
      为了现实需要,我们可以提供Combine的多个特化版本以支持任意数量的接口组合。如果仅仅是为了去掉一个IRWiter就引入一个Combine,虽有好处,但是意义也不大。那么,考虑这样一个例子。
        struct IHttpReader : IReader;
        struct IFileReader : IReader;
    我们需要一个对象,同时支持从网络和从文件读取的能力。先看不引入Combine的做法:
        struct IFileHttpReader : IFileReader , IHttpReader;
        typedef Reform<IFileHttpReader, ImpRefCount, ImpHttpReader,
            ImpFileReader>::type ConcreteRWiter;
    觉得有什么问题吗?ImpReader同时实现了IFileReader分支和IHttpReader分支中的IReader,但是,和IRefCount不同的是,我们完全有理由相信,这两个分支其实需要不同的IReader的实现。即使IReader确实可以是同样的实现,另一个严重的问题是,ImpReader是一个不完整的实现,ImpFileReader和ImpHttpReader都分别重载了IReader中的一部分方法,例如,两者都实现了如下方法:
        virtual bool open(const char* url);
    如何解决这个问题?让我们回顾一下IFileHttpReader,首先这个接口就是个问题产物:
        open到底open什么?文件,还是HTTP连接,还是两个都打开?也就是说,从概念上来讲,IFileHttpReader就存在矛盾,这样的概念很显然是难以维护的。其次,我们完全没有办法为两个分支提供不同的实现,当然,其根源是IFileHttpReader的错误设计导致的,不采用我们这里提到的技术,问题依然存在。现在引入一个结论:如果某个接口的基类树中多次出现同一个接口,我们的技术无法为这些接口分别提供不同的实现。这里的解决方案是抛弃IFileHttpReader,引入Combine, 我们可以这样解决问题:
        typedef Reform<
            Combine< ImpFileReader <IFileReader>,  ImpHttpReader <IHttpReader> >,
             ImpRefCount, ImpReader
        >::type ConcreteFileHttpReader;
    假设,ImpReader不能同时满足两个分支的要求,我们可以这么做:
        typedef Reform <
            Combine< ImpFileReader < ImpReaderA<IFileReader> >,
                ImpHttpReader < ImpReaderB <IHttpReader> >
                          >,
             ImpRefCount
        >::type ConcreteFileHttpReader;
    利用Combine,我们可以充分发挥多重继承的组合能力,我们既享受了接口设计和实现分离的好处—更容易维护概念了,也充分享有代码复用的能力。并且,将设计决策充分推迟:甚至客户程序员完全可以定制自己的接口实现从而和现有系统结合,这是一个完美的Open-Close的设计手段。

    <!--[if !supportLists]--><!--[if !supportLists]--><!--[if !supportLists]--><!--[if !supportLists]-->  现在,总结一下在多重继承中的注意事项。
        1.接口尽量是单继承的。
        2.多重继承的接口必须意识到,所有继承树的相同接口只能共享同一份实现。
        3.严苛地去维护接口的概念,不要为了实现问题定义中间的接口(就象那个IFileHttpReader)
        4.合理地利用多重继承的组合能力。<!--[if !supportEmptyParas]-->


      关于最后一条,您可以做一些有趣的探索。给出一个空基类:
        struct Over{};
    当然,也可以是其它非模板类。把所有的类都实现成模版形式:ImpClassA<T>, ImpClassB<T>,借助于Combine,我们可能给出这样的定义:
        typedef Combine<
                 ImpClassA< Combine<ImpClassB< Over >, ImpClassC< Over > > >,
                 Combine<ImpClassF<Over>, ImpClassB<ImpClassD< Over > >>,
                 ImpClassE<Over>
        >::type  ConcreteSomeClass;
    我们注重于将这些ImpClasses拆成尽可能小的正交模块。那么借助组合技术,可能获得很高的复用性。但是,有句老话,不要为了复用而复用,反正,这里的探索我也是浅尝辄止,出了什么事情和我无关。特别提醒一下,上面代码中Combine里面出现了一个type,你可以尝试在上面施加你喜欢的TMP手法嘛。


      把那些有趣的探索先放在一边。现在,我已经把这种技术完整地呈现出来了。然而,没有一项技术是完美的,这里也不例外。这个技术有两个小小的缺陷。

      第一个缺陷则是构造函数的问题。回顾Combine的实现,我们无法为Combine额外提供合适的构造函数。不过,这个问题并不是特别严重,我们完全可以定制自己的Combine。并且,这些不同的Combine可以混合使用。另外,在组装的时候需要小心的维护构造函数的调用链,这可能伤害到复用性。Assignment中也存在类似的问题。运算符重载也可能导致混乱,不过,我一直认为,在值语义之外的类当中重载运算符可是要非常谨慎的,很显然,我们这里展示的技术并不适合值语义的类型设计。

      另一个缺陷是工程上的。因为上述的实现都是模板类,所以,我们通常需要将实现在头文件里面提供,这个可能是有些人不愿意的。我将展现一种解决的方法,这就是另一个利器:Pimpl惯用法。
    以IReader为例,假设如下接口:
        struct IReader : IRefCount
        {
            virtual bool open(const char* url) = 0;
            virtual void read(Buffer& buf, size_t size) = 0;
        };

    现在,我们只实现read方法:
        class ConcreteImpReader;//前置申明
        template<typename Base>
        class ImpPartialReader : public Base
        {
            Pimpl<ConcreteImpReader> m_reader;
        public:
            ImpPartialReader() : m_reader(this), Base(){}
            virtual void read(Buffer& buf, size_t size) { m_reader->read(buf, size);}
        };

    现在,给出一个原始的Pimpl实现:
        template<typename T>
        struct Pimpl
        {
           T*  imp;
           template<typename Host>
           explicit Pimpl(Host* host)  : imp(new T(host)){}
           T* operator->() const{return imp;}
           ~Pimpl(){delete imp;}
           …
        };

      在单独的文件中实现:
        class ConcreteImpReader
        {
            ConcreteImpReader(IReader * host) : m_host(host){}
            void read(Buffer& buf, size_t size) { …}
            …
        };

        ConcreteImpReader中可以引用所在的host对象的其他方法,但是自己实现的方法除外。如果我们愿意,也可以把接口的实现分拆到多个具体的实现类当中,只是我们无法获得象多重继承那样强大的组合能力。


       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/9/19 10:47:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 理论计算机科学 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/4/25 1:31:13

    本主题贴数2,分页: [1]

     *树形目录 (最近20个回帖) 顶端 
    主题:  乌托邦式的接口和实现分离技术(16464字) - 卷积内核,2006年9月19日
        回复:  你在写什么? 没时间看 不好意思 路过一下(39字) - Templarwzy,2007年2月10日

    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    7,394.043ms