GotW #14 Class Relationships Part I
著者:Herb Sutter
翻译:kingofark
[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。
Revision 1.0
Guru of the Week 条款14:类之间的关系(上篇)
难度:5 / 10
(你的面向对象设计水平怎么样?本条款将描述一个迄今为止仍有许多程序员在犯的一个错误——唔,当然,是关于类的设计的错误)
[问题]
网络应用程序一般有两种不同的communication session(通信会话),每一个communication session(通信会话)有其自己特定的消息协议(message protocol)。当然,这两种协议也有相似之处,比如一些运算操作和一些消息(message)可能是相同的。于是,当程序员想在BasicProtocol类里面封装一些运算操作和消息的时候,就可能进行如下的设计:
class BasicProtocol /* : possible base classes */ { public: BasicProtocol(); virtual ~BasicProtocol(); bool BasicMsgA( /*...*/ ); bool BasicMsgB( /*...*/ ); bool BasicMsgC( /*...*/ ); }; class Protocol1 : public BasicProtocol { public: Protocol1(); ~Protocol1(); bool DoMsg1( /*...*/ ); bool DoMsg2( /*...*/ ); bool DoMsg3( /*...*/ ); bool DoMsg4( /*...*/ ); }; class Protocol2 : public BasicProtocol { public: Protocol2(); ~Protocol2(); bool DoMsg1( /*...*/ ); bool DoMsg2( /*...*/ ); bool DoMsg3( /*...*/ ); bool DoMsg4( /*...*/ ); bool DoMsg5( /*...*/ ); };代码中的成员函数可以通过掉用基类的函数来实施一些必要的操作,但是真正的传输操作(transmission)还是由它们自己来实施。代码中的每一个类当然都有可能还包含有其它的成员。在这里我们假定所有与问题有关的成员都已经列在我们所看到的代码中了。
试着分析代码中的这种设计。有什么需要修改的吗?如果有,说明为什么需要修改。
[解答]
本条款指出了在对“类之间的关系”之设计中,普遍存在的一个隐患。我们先回忆一下上面给出的代码:Protocol1和Protocol2这两个类以public方式派生自基类BasicProtocol;基类BasicProtocol完成一些特定的任务。
这个问题的关键在于下面的这句话:
代码中的成员函数可以通过掉用基类的函数来实施一些必要的操作,但是真正的传输操作(transmission)还是由它们自己来实施。
问题出来了:代码很清楚的描绘了一个“is implemented in terms of(根据某物实现)”关系。[译注1] 在C++中,这种关系意味着实施“private inheritance(私有继承)”或者“membership(成员归属)”。但不幸的是,许多人一直以为这种关系意味着实施“public inheritance(公有继承)”。他们把implementation inheritance(实现继承)和interface inheritance(接口继承)搞混了。事实上,implementation inheritance(实现继承)和interface inheritance(接口继承)是完全不相同的,人们对于此的迷惑正是源于我们在本条款中所要讨论的问题。[译注2][注释1]
下面的几条线索更为详细的说明了这里的问题:
1. BasicProtocol类没有提供虚拟函数(virtual function)(这里不包括那个析构函数;关于析构函数的问题,会在下面的叙述中提到)。[注释2] 这也就是说,它并不准备以polymorphically(多态)的方式来被使用,而这便意味着不应该使用公有继承(public inheritance)机制。
2. BasicProtocol类里面没有protected函数或protected成员。这也就是说,类中不存在“derivation interface(派生接口)”,而这便意味着不应该以任何方式对BasicProtocol进行继承——不管是以public方式还是以private方式。
3. BasicProtocol封装了一些特定的操作,但它并不像其派生类那样能够实施自己的传输操作。这也就是说,BasicProtocol对象即不像其派生类的对象那样工作(WORKS-LIKE-A),也不如其派生协议类的对象那样有用(USABLE-AS-A)。公有继承(public inheritance)应该只模塑(model)出一种(也是唯一一种)关系:即一个真正的接口上的IS-A(是一个)关系,它遵循Liskov substitution principle(里斯科夫替换原则)。[译注3] 为了明确起见,我通常将其称为WORKS-LIKE-A(像某物一样工作)和USABLE-AS-A(如某物一样有用)。[注释3]
4. 这些派生类仅仅只使用了BasicProtocol的公共接口(public interface)。这也就是说,它们并没有因为派生自BasicProtocol而得到什么额外的好处。我们完全可以用一个辅助性的BasicProtocol对象来完成它们所承担的任务。
这样一来,我们就需要对代码做一些修改了:
首先,既然在设计时并不打算让BasicProtocol作为基类被其它类继承,那么它的虚拟函数(virtual function)就是不必要的了,应该被去掉。其次,BasicProtocol应该改成一个诸如MessageCreator之类的名称,使其不至于误导别人,造成理解上的错误。
那么,在经过了上述修改之后,应该采用哪一种方式来模塑(model)“is implemented in terms of(根据某物实现)”这种关系呢?采用private inheritance(私有继承)方式,还是membership(成员归属)方式?
这个问题的答案很容易记住:
[学习指导]:在模塑(model)“is implemented in terms of(根据某物实现)”关系时,总是采用membership(成员归属)方式。只有在万不得已确实需要形成继承关系的时候——即当你需要能够访问protected成员,或者需要覆写(override)虚拟函数的时候——才采用private inheritance(私有继承)方式。绝不要仅仅因为要复用代码就采用public inheritance(公有继承)方式。
采用membership(成员归属)方式,使我们可以更好的把一些问题分开来考虑,因为这时候使用者之类(using class)成了一个普通的客户端,只能对受用者之类(used class)的公共接口进行访问。所以,请采用membership(成员归属)方式,这样你就会发现你的代码简洁多了、可读性强了、更易维护了。简单的说吧,你的代码会在花费方面节省多了!
*********************************************
[注释1]:碰巧的是,那些易于犯这个错误的程序员们经常会生成极深的继承层次。基于如下两个原因,这样做大大加重了维护工作的负担:
l 增加了不必要的复杂性
l 强迫用户去了解许多不必要的类接口,即使他们仅仅只是想使用某一个特定的派生类。
另外,由于增加了不必要的vtable,以及不需要使用vtable的类的间接性,这样做对内存的使用和程序性能也有影响。如果你发现自己也经常生成深层的继承层次,那你就该审视一下你的设计方法了,看看你是不是已经养成了这个坏习惯。其实,较深的层次体系很少被使用,甚至基本上就不是一个好东西……如果你不相信,并仍然认为“如果没有很多继承关系的话,就不能算是面向对象了”,那么你就应该去看看标准库——那是一个很好的例子,足可以说明你想错了。
[注释2]:即使BasicProtocol本身也是派生自另一个类,这里的结论也是不变的,因为BasicProtocol还是没有提供任何新的虚拟函数(virtual function)。如果有某个BasicProtocol的基类真的提供了虚拟函数(virtual function),那也只不过说明,是那个基类希望以polymorphically(多态)的方式来被使用,而不是BasicProtocol;于是我们至多也就应该继承自那个基类(而不是继承自BasicProtocol)。
[注释3]:的确,当你以public方式进行继承从而得到一个接口的时候,如果基类既有你需要的接口,又有其特定的某些实现,那么其中的一些实现你会附带着得到。这种效果几乎总是可以在设计上予以回避,但也并不总是需要采取特别纯正的“one responsibility per class(一个类一个职责)”这样的方法。
[译注1]:关于“is implemented in terms of”,请参看Scott Meyers《Effective C++》条款40和条款42。
[译注2]:请参看Scott Meyers《Effective C++》条款36:区分interface inheritance(接口继承)和implementation inheritance(实现继承)。
[译注3]:关于Liskov substitution principle(里斯科夫替换原则),请参看侯捷先生译的Scott Meyers《Effective C++》条款35中相关的讨论。
(完)