网络应用程序有两种会话,各自都有其自身的消息协议。这两个协议具有相似性(某些运算、乃至某些讯息相同),因此这个程序员提成如下设计,在一个BasicProtocol类中封装了这些共同的计算和消息。
class BasicProtocol
{
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(/*...*/);
};
每一个DoMsg...() member function 都调用BasicProtocol::Basic...()执行共同的工作,然后DoMsg...()另外再执行其他实际传送工作。每一个class都可以有其他的members,但你可以假设所有重要的members都已呈现出来。
请评论此设计,有任何东西需要改变吗?如果有,为什么?
【解答】
这个条款以实例说明了一个十分常见的OO classes相互关系上的错误设计。回顾一下,classes Protocol1和Protocol2以public方式继承了一个共同的base class BasicProtocol,然后继续执行某些共同的工作。
问题的关键在于以下这段描述:
每一个DoMsg() member function都调用BasicProtocol::Basic..()执行共同的工作,然后DoMsg...() 另外再执行其他的实际的传送工作。
这明显是在描述一个【以某物为基础实现】(is implemented in terms of )的关系,这种关系在C++中应该以private inheritance或membership来完成。
不幸的是许多人往往误用public inheritance,因为将implementation inheritance(实现继承)和interface inheritance(接口继承)混淆在一起。两种并非同相同的东西,这种混淆就是这个问题的根源。
常见错误:绝不要以public inheritance,除非你要模塑的是真正的Liskov IS-A和WORK-LIKE-A的关系,所有被继承的member function不能要求更多的也不能承诺更少。
顺便提及:习惯这种错误(使用共有继承来实现)的程序员通常会创建很深的继承层次。这样做大大增加了维护负担,因为它增加了不必要的复杂性,即使用户只是想做一个特定的派生类;另外,这也影响内存使用和程序性能,因为它将不必要的虚函数表(vtable)和间接性强加到实际并不需要的类中。如果你发现自己频繁使用创建深的继承层次,你应该重新审视你的类的风格。
设计准则:绝不要以public inheritance复用(reuse)base class内的代码;public inheritance是为了(被多态性的使用基类对象的代码)重用。
下面更详细的说明,阐明这个问题:
- BasicProtocol没有提供任何的虚函数(以及destructor,此点稍后讨论),这意味着它并不意图以多态方式被使用,这对public inheritance 是一个强烈的反对暗示。
- BasicProtocol没有任何protected members,这意味着没有任何派生接口(derivation interface),这对任何继承(不论public或private),都是一个强烈的反对暗示。
- BasicProtocol封装了共同的工作,但是一如先前所述,它并不像derived classes那样执行自己的传送动作。这意味着BasicProtocol并非WORK-LIKE-A(像一个)衍生出来的protocol,也并非USABLE-AS-A(使用起来可视为)衍生的protocol。public inheritance应该用来模塑唯一一件事情:一个真正的IS-A关系,奉行Liskov替换原则。为了让文意更清晰,通常称替换原则为WORKS-LIKE-A或USABLE-AS-A。
- 所有派生下去的classes 只使用BasicProtocol的public接口。这意味着它们并不因为自己的derived classes而受益,它们的工作可以轻松的使用一个BasicProtocol类型的辅助对象来完成。
这意思是,我们在清理方面的有一些工作要做:首先,由于BasicProtocol很明显并非用来让其他class继承的,所以其virtual destructor没有必要,应该去除;其次,BasicProtocol应该重新命名(例如MessageCreator或MessageHelper)以避免误会。
设计准则:当我们希望模塑出“is implemented in terms of”的关系,请选择membership/aggregation而不要使用inheritance。只要在绝对必要的情况下才使用private inheritance——也就是说当你需要存取protected members或是需要重载虚函数时,绝不要为了复用代码而使用public inheritance。
如果使用membership,可以强迫事情有比较好的区分,因为class是一个一般的client端,只能取用被使用者的公开接口。采用了它,你会发现你的代码比较干净,比较容易阅读,也比较好维护。简单的说,你的代码成本降低了。