来源 DCI in C++ [推荐阅读]
https://www.jianshu.com/p/bb9c35606d29
C++中通过多重继承来实现DCI架构的方式,是一种几近完美的一种方式。如果非要说缺点,只有一个,就是多重继承造成的物理依赖污染问题。由于C++中要求一个类如果继承了另一个类,当前类的文件里必须包含被继承类的头文件。这就导致了领域对象类的声明文件里面事实上包含了所有它继承下来的role的头文件。在context中使用某一个role需用领域对象做cast,所以需要包含领域对象类的头文件。那么当领域对象上的任何一个role的头文件发生了修改,所有包含该领域对象头文件的context都得要重新编译,无关该context是否真的使用了被修改的role。解决该问题的一个方法就是再建立一个抽象层专门来做物理依赖隔离。例如对上例中的Human
,可以修改如下:
DEFINE_ROLE(Human)
{
HAS_ROLE(Worker);
};
struct HumanObject : Human
, private Worker
, private HumanEnergy
{
private:
IMPL_ROLE(Worker);
IMPL_ROLE(Energy);
};
struct HumanFactory
{
static Human* create()
{
return new HumanObject;
}
};
TEST(...)
{
Human* human = HumanFactory::create();
human->ROLE(Worker).produce();
ASSERT_EQ(1, human->ROLE(Worker).getProduceNum());
delete human;
}
为了屏蔽物理依赖,我们把Human
变成了一个纯接口类,它里面声明了该领域对象可被context访问的所有public role,由于在这里只用前置声明,所以无需包含任何role的头文件。而对真正继承了所有role的领域对象HumanObject
的构造隐藏在工厂里面。Context中持有从工厂中创建返回的Human
指针,于是context中只用包含Human
的头文件和它实际要使用的role的头文件,这样和它无关的role的修改不会引起该context的重新编译。
事实上C++语言的RTTI特性同样可以解决上述问题。该方法需要领域对象额外继承一个公共的虚接口类。Context持有这个公共的接口,利用dynamic_cast
从公共接口往自己想要使用的role上去尝试cast。这时context只用包含该公共接口以及它仅使用的role的头文件即可。修改后的代码如下:
DEFINE_ROLE(Actor)
{
};
struct HumanObject : Actor
, Worker
, private HumanEnergy
{
private:
IMPL_ROLE(Energy);
};
struct HumanFactory
{
static Actor* create()
{
return new HumanObject;
}
};
TEST(...)
{
Actor* actor = HumanFactory::create();
Worker* worker = dynamic_cast<Worker*>(actor);
ASSERT_TRUE(__notnull__(worker));
worker->produce();
ASSERT_EQ(1, worker->getProduceNum());
delete actor;
}
上例中我们定义了一个公共类Actor
,它没有任何代码,但是至少得有一个虚函数(RTTI要求),使用DEFINE_ROLE
定义的类会自动为其增加一个虚析构函数,所以Actor
满足要求。最终领域对象继承Actor
,而context仅需持有领域对象工厂返回的Actor
的指针。Context中通过dynamic_cast
将actor
指针转型成领域对象身上其它有效的public role,dynamic_cast
会自动识别这种转换是否可以完成,如果在当前Actor
的指针对应的对象的继承树上找不到目标类,dynamic_cast
会返回空指针。上例中为了简单把所有代码写到了一起。真实场景下,使用Actor
和Worker
的context的实现文件中仅需要包含Actor
和Worker
的头文件即可,不会被HumanObject
继承的其它role物理依赖污染。
通过上例可以看到使用RTTI
的解决方法是比较简单的,可是这种简单是有成本的。首先编译器需要在虚表中增加很多类型信息,以便可以完成转换,这会增加目标版本的大小。其次dynamic_cast
会随着对象继承关系的复杂变得性能底下。所以C++编译器对于是否开启RTTI
有专门的编译选项开关,由程序员自行进行取舍。
[最后!]DCI框架中提供了一种RTTI
的替代工具,它可以模仿完成类似dynamic_cast
的功能,但是无需在编译选项中开启RTTI
功能。这样当我们想要在代码中小范围使用该特性的时候,就不用承担整个版本都因RTTI
带来的性能损耗。利用这种替代技术,可以让程序员精确地在开发效率和运行效率上进行控制和平衡。
UNKNOWN_INTERFACE(Worker, 0x1234)
{
// Original implementation codes of Worker!
};
struct HumanObject : dci::Unknown
, Worker
, private HumanEnergy
{
BEGIN_INTERFACE_TABLE()
__HAS_INTERFACE(Worker)
END_INTERFACE_TABLE()
private:
IMPL_ROLE(Energy);
};
struct HumanFactory
{
static dci::Unknown* create()
{
return new HumanObject;
}
};
TEST(...)
{
dci::Unknown* unknown = HumanFactory::create();
Worker* worker = dci::unknown_cast<Worker>(unknown);
ASSERT_TRUE(__notnull__(worker));
worker->produce();
ASSERT_EQ(1, worker->getProduceNum());
delete unknown;
}
通过上面的代码,可以看到CUB的dci框架中提供了一个公共的接口类dci::Unknown
,该接口需要被领域对象public继承。能够从dci::Unknown
被转化到的目标role需要用UNKNOWN_INTERFACE
来定义,参数是类名以及一个32位的随机数。这个随机数需要程序员自行提供,保证全局不重复(可以写一个脚本自动产生不重复的随机数,同样可以用脚本自动校验代码中已有的是否存在重复,可以把校验脚本作为版本编译检查的一部分)。领域对象类继承的所有由UNKNOWN_INTERFACE
定义的role都需要在BEGIN_INTERFACE_TABLE()
和END_INTERFACE_TABLE()
中由__HAS_INTERFACE
显示注册一下(参考上面代码中HumanObject
的写法)。最后,context持有领域对象工厂返回的dci::Unknown
指针,通过dci::unknown_cast
将其转化目标role使用,至此这种机制和dynamic_cast
的用法基本一致,在无法完成转化的情况下会返回空指针,所以安全起见需要对返回的指针进行校验。
上述提供的RTTI替代手段,虽然比直接使用RTTI略显复杂,但是增加的手工编码成本并不大,带来的好处却是明显的。例如对嵌入式开发,这种机制相比RTTI来说对程序员是可控的,可以选择在仅需要该特性的范围内使用,避免无谓的内存和性能消耗。