乌托邦式的接口和实现分离技术.docx
- 文档编号:23126912
- 上传时间:2023-05-08
- 格式:DOCX
- 页数:15
- 大小:23.20KB
乌托邦式的接口和实现分离技术.docx
《乌托邦式的接口和实现分离技术.docx》由会员分享,可在线阅读,更多相关《乌托邦式的接口和实现分离技术.docx(15页珍藏版)》请在冰豆网上搜索。
乌托邦式的接口和实现分离技术
乌托邦式的接口和实现分离技术
发布时间:
2007-04-2908:
00:
00 来源:
作者:
点击:
281
《ImperfectC++》中展示了一种叫“螺栓”的技术,然而,这本书中的讨论并不足够深入。
当然,我也相信Matthew是故意的,从而让我们这些“三道贩子”(Matthew自称是二道贩子)也能够获得一点点成就感。
考虑这样一个接口设计:
structIRefCount;
structIReader:
publicIRefCount;
在Reader中实现接口:
<!
--[if!
supportEmptyParas]-->classReader:
publicIReader;
在上述的继承结构中,IRefCount是一个结构性的类,用来实现引用计数,实际上它和领域逻辑部分IReader没有什么关系。
我们打算在IRefCount的基础上,建立了一套工具来管理对象生命周期和帮助实现异常安全的代码(例如,smartpointer)。
现在来考虑Reader的实现,Reader除了需要实现IReader的接口,还必须实现IRefCount的接口。
这一切看起来似乎顺理成章,让我们继续看下面的设计<!
--[if!
supportEmptyParas]-->:
structIWriter:
publicIRefCount;
<!
--[if!
supportEmptyParas]-->classWriter:
publicIWriter;
现在来考虑Writer的实现,和Reader一样,Writer除了要实现IWriter的接口外,同时还需要实现IRefCount的接口。
现在,我们来看看IRefCount是如何定义的:
structIRefCount{
virtualvoidadd()=0;
virtualvoidrelease()=0;
virtualintcount()const=0;
virtualvoiddispose()=0;
virtual~IRefCount(){}
};
在Reader中的IRefCount的实现:
virtualvoidadd(){++m_ref_count;}
virtualvoidrelease(){--m_ref_count;}
virtualintcount()const{returnm_ref_count;}
virtualvoiddispose(){deletethis;}
…
intm_ref_count;
同样,在Writer的实现中,也包含了一模一样的代码,这违背了DRY原则(Don’tRepeatYourself)。
况且,随着系统中的类增加,大家都意识到,需要将这部分代码复用。
一个能够工作的做法是把IRefCount的实现代码直接放到IRefCount中去实现,通过继承,派生类就不必再次实现IRefCount了。
我们来看一下dispose的实现:
virtualvoiddispose(){deletethis;}
这里,采用了delete来销毁对象,这就意味着Reader必须在堆上分配,才可能透过IRefCount正确管理对象的生命周期,没关系,我们还可以overridedispose方法,在Reader如下实现dispose:
virtualvoiddispose(){}
但是,这样又带来一个问题,Reader不能被分配在堆上了!
如果你够狠,当然,你也可以这么解决问题:
classHeapReader:
IReader;
classStackReader:
HeapReader{virtualvoiddispose(){}};
问题是,StackReader是一个HeapReader吗?
为了代码复用,我们完全不管什么概念了。
当然,如果你和我一样,看重维护概念,那么这么实现吧:
classHeapReader:
IReader;
classStackReader:
IReader;
这样一来,IReader的实现将被重复,又违背了DRY原则,等着被将来维护的工程师诅咒吧!
或许,那个维护工程师就是3个月后的你自己。
如果这样真的能够解决问题,那么也还是可以接受的,很快,我们有了一个新的接口:
structIRWiter:
IReader,IWriter;
classRWiter:
publicIRWiter;
考虑一下IRefCount的语义:
它用来记录对所在对象的引用计数。
很显然,我从IReader和IWriter中的任意一个分支获得的IRefCount应该都是获得一样的引用计数效果。
但是现在,这个继承树存在两个IRefCount的实例,我们不得不在RWiter当中重新重载一遍。
这样,从IReader和IWriter继承来的两个实例就作废了,而且,我们可能还浪费了8个字节。
为了解决这个问题,我们还可以在另一条危险的道路上继续前进,那就是虚拟继承:
structIReader:
virtualpublicIRefCount;
structIWriter:
virtualpublicIRefCount;
还记得大师们给予的忠告吗--“不要在虚基类中存放数据成员”。
“这样有什么问题吗,我们不必对大师盲目崇拜”,你一定也听过这样的建议。
如果大师们不能说服这些人,那么我也不能。
于是,我们进一步在所有的接口中提供默认实现,包括IReader和IWriter.
现在的问题是:
structIRWiter:
IReader,IWriter;
还是
structIRWiter:
virtualIReader,virtualIWriter?
如果你没有选择virtual,那么IRWiter被派生后,那么派生类的继承树中可能存在多个IReader实现,如果这个派生类要求只能提供一份IReader的语义怎么办?
除了重新实现接口还能怎样?
反过来,如果我们选择了virtual继承,那么派生类需要多个实现怎么办?
真是个麻烦事。
“这是典型的过度设计,我们为什么要考虑这么多?
”你可以这么说,但事实上,即使是一个数百文件的小型系统,也完全可能迫使你作出选择。
虽然,我们仍然有办法作出挽救措施,但是也只是苟延残喘而已。
正如我前面所说,这是一个危险的道路,聪明如你,是断然不会让自己陷入这样的泥潭的。
让我们离开虚拟继承,先回到重复代码的问题上来。
有没有更好的解决办法呢?
还好,在C++的世界里,我们有神奇的template,让我们来消除重复的代码:
template<typenameBase>
classImpReader:
publicBase{
constraint(is_base_derive(IReader,Base))
ImplementationIReader
<!
--[if!
supportEmptyParas]-->};
classHeapReader:
ImpReader<IReader>{};
classStackReader:
ImpReader<IReader>{
virtualvoiddispose(){};
<!
--[if!
supportEmptyParas]-->};
请注意,我们还是假设IRefCount已经提供了一个默认实现。
现在,情况好了很多,所有的代码都只有一份,而且,概念也没有被破坏。
假设,Writer也同样需要类似的能力,那么,我们又多了StackWriter和HeapWriter.事实上,真的有人用到了StackWriter吗?
我不知道,只是,提供了StackReader,没有理由不提供StackWriter啊。
让我们继续。
现在,我们发现,需要改进内存分配的性能问题,于是,我们希望通过内存池来分配对象,相应的dispose也需要修改:
virtualvoiddispose(){distory(this);}
于是,我们又多出两个类,PoolReader和PoolWriter。
这真是糟糕,组合爆炸可不是什么好兆头。
从我们前述的变化来看,都是IRefCount在变化,为什么不把这种变化分离出来呢?
不必为IRefCount提供默认实现,借鉴ImpReader的手法:
template<typenameBase>
classImpHeapRefCount:
publicBase{
constraint(is_base_derive(IRefCount,Base));
..};
类似的:
template<typenameBase>classImpStackRefCount:
publicBase;
<!
--[if!
supportEmptyParas]-->template<typenameBase>classImpPoolRefCount:
publicBase;<!
--[endif]-->
再看看,我们如何实现所有的Reader.
typedefImpReader<ImpHeapRefCount<IReader>>HeapReader;
typedefImpReader<ImpStackRefCount<IReader>>StackReader;
typedefImpReader<ImpPoolRefCount<IReader>>PoolReader;
以HeapReader为例,实际的继承关系是这样的:
ImpReaderàImpHeapRefCountàIReaderàIRefCount;
对于Writer,我们完全可以采取同样的手法来实现。
对于上述的typedef可以预先定义,也完全可以不定义,交给最终用户去组装吧。
现在,类的设计者再也不必为选择实现而痛苦了,你只要提供不同的砖头,客户程序员可以轻而易举的建立起大厦。
还有比这更让一个设计师幸福的吗?
继续深入,考察ImpHeapRefCount和ImpStackRefCount的实现,我们提到,dispose方法的实现是不一样的,但是,其他部分:
add,releasee和count的实现完全可以相同。
然而我们现在又分别实现了一遍,为了不违背DRY原则,我们如下处理:
template<typenameBase>
classImpPartialRefCount:
publicBase{
//实现add,release和count.
};
template<typenameBase>
classImpHeapRefCount:
publicBase{
virtualvoiddispose(){deletethis;}
};
template<typenameBase>
classImpStackRefCount:
publicBase{
virtualvoiddispose(){}
};
然后,我们可以这样定义Reader:
typedefImpReader<ImpHeapRefCount<ImpPartialRefCount<Ireader>>>HeapReader;
请注意,我们在这里展示了一种能力,不必在一个实现当中完整的实现整个接口,可以把一个接口的实现分拆到多个实现当中。
这个能力是非凡的,借助于此,我们可以提供更小粒度的实现单位,给最终用户来组装。
具体拆分到什么样的程度完全取决于代码复用的需求,以及概念维护的需要。
我们提供了高度的复用能力,同时避免了继承带来的强耦合,以及对推迟设计决策的支持,这些能力对于软件设计师而言,正如Matthew在《ImperfectC++》中所说的,这简直就是现实中的乌托邦!
现在我们把这种手法首先针对单继承做一个小结。
对于任意的接口IInterface,我们提供如下的实现:
template<typenameBase>
classImpInterface:
publicBase{
constraint(is_base_derive(IInterface,Base));
};
请注意,一个接口可以有任意多个实现,并且可以是任意的部分实现。
<!
--[if!
supportEmptyParas]--><!
--[endif]-->
假设我们有如下接口继承树:
InterfaceNàInterfaceN_1àInterfaceN_2à…àInterface0
并且提供了实现类ImpInterface0~ImpInterfaceN.
那么,InterfaceN的实例类型就是:
typedefImpInterfaceN<
ImpInterfaceN_1<
ImpInterfaceN_2<
…
ImpInterface0<InterfaceN>…>>>ConcreteClassN;
我们注意到,定义ConcreteClassN的时候,我们的ImpInterface是按照顺序来的,我认为这是合适的做法。
当然了,最后组装的权力已经交给客户了,客户爱怎么组装就怎么组装吧。
然而我们还是有一些需要注意的问题。
1.假定,我需要在ImpInterfaceI中引用基类的方法,记住,不要使用这样的手法:
ImpInterfaceI_K:
:
SomeMethod();
这样调用不具有多态性,而应该这样:
this->SomeMethod();
2.不要在自己的ImpInterfaceI实现中覆盖基类接口的其他已经实现的方法,如果你一定要这么做,那么务必在文档中说明,因为在组装的时候,顺序将是极其关键的了。
3.这个方法和设计模式中的TemplatePattern目的是不一样的。
TemplatePattern是在基类中定义了一个算法,让派生类定制算法的某些步骤。
这里的方法针对的是接口模型的概念,提供接口和实现分离的技术。
关于第二条,应该尽量避免发生。
这里说的覆盖是指基类实现已经实现了该方法,而后继实现又覆盖该方法。
基类实现可以是一个部分实现,对于没有实现的那些方法,在派生接口的实现类中实现则是常见的。
一方面,我们尽量合理分解层次之间的功能,另一个方面,可以通过定制实现模板类,来保证顺序。
尽可能的让语言本身来保证正确性,而不是依赖文档。
我们可以像这样预先装配一些东西:
template<typenameBase>
classSomeComponent:
publicImpPartA<ImpPartB<Base>>{};
可惜,C++暂时还不支持模板的不完全typedef,否则,我们还可以如下定以:
template<typenameBase>
typedefImpPartA<ImpPartB<Base>>SomeComponent;
不过,C0x很可能会支持类似的语法。
这样,我们使用SomeComponent来作为一个预制品,来保证一些安全性,而不必完全依赖文档了。
<!
--[endif]-->
看看ConcreteClassN的定义,也许你和我一样,并不喜欢这种嵌套的、递归的定义方式,太难看了。
让世界稍微美好一点吧!
于是我们提供一个辅助类:
<!
--[if!
supportEmptyParas]-->template<typenameT>structEmpty{};
template<typenameI,typenametemplate<class>classB>
structMerge{<!
--[if!
supportEmptyParas]-->typedefB<I>type;};
template<typenameI>
structMerge<I,Empty>{
typedefItype;
<!
--[if!
supportEmptyParas]-->};<!
--[endif]-->
template
<
typenameI,
typenametemplate<class>classB1,
typenametemplate<class>classB2=Empty,
…
typenametemplate<class>classBn=Empty,
>
structReform{
typedeftypenameMerge<
typenameMerge<
typenameMerge<I,B1>:
:
type
B2>:
:
type,…,Bn>:
:
typetype;
};
现在,我们可以这样定义ConcreteClassN了:
TypedefReform<InterfaceN,ImpInterface0,ImpInterface1,
…ImpInterfaceN>:
:
typeConcreteClassN;
是不是清爽了很多?
在继续下面内容以前,请回味一下这个不是问题的问题:
假设IReader有3种实现,IRefCount有3种实现,我们将如何漂亮地解决掉他们。
<!
--[endif]-->
现实世界总是要复杂得多,让我们进入真实的世界。
回顾这个接口:
structIRWiter:
IReader,IWriter;
假设我们确实需要IReader,IWriter,但是并不需要IRWrite,可不可以让一个对象同时支持这两个接口呢,就像COM一样?
当然可以,我们借助于这样一个辅助模版:
template<typenameB1,typenameB2>
structCombine:
B1,B2{
typedefB1type1;
typedefB1type2;
<!
--[if!
supportEmptyParas]-->};<!
--[endif]-->
typedefReform<Combine<IReader,IWriter>,ImpRefCount,ImpWriter,ImpReader>:
:
typeConcreteRWiter
为了现实需要,我们可以提供Combine的多个特化版本以支持任意数量的接口组合。
如果仅仅是为了去掉一个IRWiter就引入一个Combine,虽有好处,但是意义也不大。
那么,考虑这样一个例子。
structIHttpReader:
IReader;
structIFileReader:
IReader;
我们需要一个对象,同时支持从网络和从文件读取的能力。
先看不引入Combine的做法:
structIFileHttpReader:
IFileReader,IHttpReader;
typedefReform<IFileHttpReader,ImpRefCount,ImpHttpReader,
ImpFileReader>:
:
typeConcreteRWiter;
觉得有什么问题吗?
ImpReader同时实现了IFileReader分支和IHttpReader分支中的IReader,但是,和IRefCount不同的是,我们完全有理由相信,这两个分支其实需要不同的IReader的实现。
即使IReader确实可以是同样的实现,另一个严重的问题是,ImpReader是一个不完整的实现,ImpFileReader和ImpHttpReader都分别重载了IReader中的一部分方法,例如,两者都实现了如下方法:
virtualboolopen(constchar*url);
如何解决这个问题?
让我们回顾一下IFileHttpReader,首先这个接口就是个问题产物:
open到底open什么?
文件,还是HTTP连接,还是两个都打开?
也就是说,从概念上来讲,IFileHttpReader就存在矛盾,这样的概念很显然是难以维护的。
其次,我们完全没有办法为两个分支提供不同的实现,当然,其根源是IFileHttpReader的错误设计导致的,不采用我们这里提到的技术,问题依然存在。
现在引入一个结论:
如果某个接口的基类树中多次出现同一个接口,我们的技术无法为这些接口分别提供不同的实现。
这里的解决方案是抛弃IFileHttpReader,引入Combine,我们可以这样解决问题:
typedefReform<
Combine<ImpFileReader<IFileReader>,ImpHttpReader<IHttpReader>>,
ImpRefCount,ImpReader
>:
:
typeConcreteFileHttpReader;
假设,ImpReader不能同时满足两个分支的要求,我们可以这么做:
typedefReform<
Combine<ImpFileReader<ImpReaderA<IFileReader>>,
ImpHttpReader<ImpReaderB<IHttpReader>>
>,
ImpRefCount
>:
:
typeConcreteFileHttpReader;
利用Combine,我们可以充分发挥多重继承的组合能力,我们既享受了接口设计和实现分离的好处?
更容易维护概念了,也充分享有代码复用的能力。
并且,将设计决策充分推迟:
甚至客户程序员完全可以定制自己的接口实现从而和现有系统结合,这是一个完美的Open-Close的设计手段。
<!
--[if!
supportLists]--><!
--[if!
supportLists]--><!
--[if!
supportLists]--><!
--[if!
supportLists]-->现在,总结一下在多重继承中的注意事项。
1.接口尽量是单继承的。
2.多重继承的接口必须意识到,所有继承树的相同接口只能共享同一份实现。
3.严苛地去维护接口的概念,不要为了实现问题定义中间的接口(就象那个IFileHttpReader)
4.合理地利用多重继承的组合能力。
<!
--[if!
supportEmptyParas]-->
关于最后一条,您可以做一些有趣的探索。
给出一个空基类:
structOver{};
当然,也可以是其它非模板类。
把所有的类都实现成模版形式:
ImpClassA<T>,ImpClassB<T>,借助于Combine,我们可能给出这样的定义:
typedefCombine<ImpClassA<Combine<ImpClassB<Over>,ImpClassC<Over>>>,Combine<ImpClassF<Over>,ImpClassB<ImpClassD<Over>>>,ImpClassE<O
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 乌托邦 接口 实现 分离 技术