我有一个设计问题一直困扰我一段时间,但我找不到一个好的(在OOP意义上)解决方案。语言是C ++,我一直回到RTTI - 这通常被称为糟糕设计的指标。
假设我们将一组不同类型的模块实现为不同的类。每种模块的特征在于定义的接口,但是实现可以变化。 因此,我的第一个想法是为每种模块(例如IModuleFoo,IModuleBar等)创建一个接口(纯抽象)类,并在单独的类中创建实现。到目前为止一切都很好。
class IModuleFoo {
public:
virtual void doFoo() = 0;
};
class IModuleBar {
public:
virtual void doBar() = 0;
};
另一方面,我们有一组(应用程序)类,每个类都使用其中的几个模块,但只能通过接口 - 甚至模块本身也可能使用其他模块。但是,所有应用程序类将共享相同的模块池。我的想法是为所有模块创建一个管理器类(ModuleManager),应用程序类可以查询它们所需的模块类型。可用模块(以及具体实现)是在管理器初始化期间设置的,并且可能随时间而变化(但这不是我的问题的一部分)。
由于不同模块类型的数量很可能> 10并且可能随着时间的推移而增加,因此我似乎不适合将它们分别存储到它们的引用(或指针)。此外,管理员可能需要在所有托管模块上调用几个函数。因此,我创建了另一个接口(IManagedModule),其好处是我现在可以使用IManagedModules的容器(list,set,无论如何)将它们存储在管理器中。
class IManagedModule {
public:
virtual void connect() = 0;
{ ... }
};
结果是,应该管理的模块需要从IManagedModule和适当的接口继承它的类型。
但是当我想到ModuleManager时,事情会变得丑陋。可以假设每次存在每个模块类型最多一个实例。因此,如果有可能做这样的事情(管理器是ModuleManager的实例),一切都会好的:
IModuleFoo* pFoo = manager.get(IModuleFoo);
但我很确定不是。我还想到了一个基于模板的解决方案,如:
IModuleFoo* pFoo = manager.get<IModuleFoo>();
这可行,但我不知道如何在管理器中找到正确的模块,如果我拥有的是一组IManagedModules - 当然不使用RTTI。
一种方法是为IManagedModule提供虚拟 getId()方法,依靠实现为每种模块使用非ambidous id并自行执行指针转换。但这只是重新发明轮子(即RTTI)并且需要在实施类中提供大量的纪律(提供正确的ID等等),这是不可取的。
长话短说 - 问题是这里是否真的没有办法解决某种RTTI问题,在这种情况下,RTTI甚至可能是一个有效的解决方案,或者是否有更好的(更清洁,更安全......)设计它表现出相同的灵活性(例如应用程序类和模块类之间的松散耦合......)?我错过了什么吗?
答案 0 :(得分:1)
听起来你正在寻找与COM QueryInterface类似的东西。现在,您不需要完全实现COM,但基本原则是:您有一个带有虚函数的基类,您可以向其传递一个标识符,指定您想要的接口。然后虚函数查看它是否可以实现该接口,如果是,则传回指向该接口的指针。
例如:
struct IModuleBase {
// names changed so as not to confuse later programmers with true COM
virtual bool LookupInterface(int InterfaceID, void **interfacePtr) = 0;
// Easy template wrapper
template<typename Interface>
Interface *LookupInterface() {
void *ptr;
if (!LookupInterface(Interface::INTERFACE_ID, &ptr)) return NULL;
return (Interface *)ptr;
}
};
struct IModuleFoo : public IModuleBase {
enum { INTERFACE_ID = 42 };
virtual void foo() = 0;
};
struct SomeModule : public IModuleFoo {
virtual bool LookupInterface(int interface_id, void **pPtr) {
switch (interface_id) {
case IModuleFoo::INTERFACE_ID:
*pPtr = (void*)static_cast<IModuleFoo *>(this);
return true;
default:
return false;
}
}
virtual void foo() { /* ... */ }
};
它有点笨拙,但它并不太糟糕,没有RTTI你除了这样的方法之外别无选择。
答案 1 :(得分:0)
我认为bdonlan's suggestion很好,但要求每个模块类型声明一个不同的INTERFACE_ID
是一个维护问题。通过让每个模块类型声明一个静态对象并使用其地址作为ID:
struct IModuleFoo : public IModuleBase {
static char distinct_; // Exists only to occupy a unique address
static const void *INTERFACE_ID;
virtual void foo() = 0;
};
// static members need separate out-of-class definitions
char IModuleFoo::distinct_;
const void *IModuleFoo::INTERFACE_ID = &distinct_;
在这种情况下,我们使用void *
作为接口ID类型,而不是int
或枚举类型,因此其他一些声明中的类型将需要更改。
此外,由于C ++中的怪癖,INTERFACE_ID
值尽管被标记为const
,但并不“足够常量”,无法用于case
中的switch
标签语句(或数组大小声明,或少数其他地方),因此您需要将switch
语句更改为if
。如标准第5.19节所述,case
标签需要整数常量表达式,粗略地说,编译器可以通过查看当前的转换单元来确定它;虽然INTERFACE_ID
仅仅是常量表达式,但在链接时间之前无法确定其值。