《Imperfect C++》中展示了一种叫“螺栓”的技术,然而,这本书中的讨论并不足够深入。当然,我也相信Matthew是故意的,从而让我们这些“三道贩子”(Matthew自称是二道贩子)也能够获得一点点成就感。
考虑这样一个接口设计:
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.