使用抽象工厂的问题
我正在使用抽象工厂创建用户界面组件,如对话框。所使用的抽象工厂从当前选择的通用“INode”返回,INode是几种不同类型节点的基类。因此,举例来说,如果我想添加相同类型选择节点的新节点,该方案是这样的:使用抽象工厂的问题
(请注意,这是半伪代码)
用户点击节点,节点被存储供以后使用:
void onTreeNodeSelected(INode *node)
{
selectedNode = node;
}
用户点击用户界面上的“添加”:
void onAddClicked()
{
IFactory *factory = selectedNode->getFactory();
Dialog *dialog = factory->createAddDialog(parentWidget);
dialog->show();
}
这一切似乎罚款。问题出现在我想编辑选定节点时:
void onEditClicked()
{
IFactory *factory = selectedNode->getFactory();
Dialog *dialog = factory->createEditDialog(selectedNode, parentWidget);
dialog->show();
}
哦,亲爱的..我传入一个INode对象。在某些时候,我将不得不将它们转换为正确的节点类型,以便对话框可以正确使用它。
我研究过“PostgreSQL Admin 3”的源代码,他们做了类似的事情。他们通过做这样的事情来绕过它:
FooObjectFactoryClass::createDialog(IObject *object)
{
FooObjectDialog *dialog = new FooObjectDialog((FooObject*)object);
}
Yeck .. cast!
我能想到绕过它,仍然可以使用我的工厂唯一的办法是到节点自身注入到工厂返回之前:
FooNode : INode
{
FooNodeFactory* FooNode::getFactory()
{
fooNodeFactory->setFooNode(this);
return fooNodeFactory;
}
}
所以后来我的编辑事件可以这样做:
void onEditClicked()
{
IFactory *factory = selectedNode->getFactory();
Dialog *dialog = factory->createEditDialog(parentWidget);
dialog->show();
}
它将使用注入的节点作为上下文。
我想如果没有注入代码,createEditDialog可能会声明错误或其他东西。
有什么想法?
谢谢!
一个常见的解决方案是“double-dispatch”,您可以在一个对象上调用虚拟函数,然后在另一个对象上调用虚拟函数,并传递this
,该函数现在具有正确的静态类型。所以,在你的情况下,工厂可以包含各种类型的对话“创造”功能:
class IFactory
{
public:
....
virtual Dialog* createEditDialog(ThisNode*, IWidget*);
virtual Dialog* createEditDialog(ThatNode*, IWidget*);
virtual Dialog* createEditDialog(TheOtherNode*, IWidget*);
....
};
那么每个类型的节点有一个虚拟createEditDialog
一个分派到正确的工厂函数:
class INode
{
public:
....
virtual Dialog* createEditDialog(IWidget* parent) = 0;
....
};
class ThisNode : public INode
{
public:
....
virtual Dialog* ThisNode::createEditDialog(IWidget* parent)
{
return getFactory()->createEditDialog(this, parent);
}
....
};
然后你就可以创建正确的对话
void onEditClicked()
{
Dialog *dialog = selectedNode->createEditDialog(parentWidget);
dialog->show();
}
在我看来,只要您的代码得到正确评论,使用C风格演员(尽管C++风格将是首选)是完全可以接受的。
我不是DI(dependency injection)的粉丝,因为它使一些代码难以遵循,在你的情况下,我宁愿看看dynamic_cast<>()
或其他东西,而不是试图在多个源文件中注入代码。
我会消化两件事。
第一:铸造没有错。如果你想要安全,你可以在INode类中使用RTTI(type_id stuff)或一些虚函数,它可以返回一些信息,让你知道它是否安全。其次:你可以检查createEditDialog函数需要什么,并将它们放在INode或者是类型为createDialog的继承类中。
总的来说,我没有看到你描述的问题真的有问题,没有看到整个代码很难给出更多的建议,我认为这是不可行的。
你的节点注入到工厂的方法通常是我发现有用的模式,但也有常当你在创建工厂时没有对目标对象的引用时,就像你在这里做的那样。因此,在这种情况下,这可能适合您,在一般情况下比处理这类问题更简单。
对于更一般的情况,您需要使用接口的概念并建立一个机制,通过该机制,您的对象可以发布它支持的接口并为客户端提供对这些接口的访问。完全动态地做到这一点导致类似于COM的方法需要动态注册和投射。但是,如果您想要公开一组相对稳定的接口,并且在需要添加新的组件接口时可以编辑接口,您也可以以静态类型的方式执行此操作。
因此,这将是怎样做简单的静态类型的方法的例子:
struct INode
{
virtual INodeSize* getNodeSizeInterface() = 0;
virtual INodeProperties* getNodePropertiesInterface() = 0;
virtual INodeColor* getNodeColorInterface() = 0;
... // etc
}
现在每个INode
实现可以返回部分或全部组件接口(它只是返回NULL,如果它没”实施它们)。然后,您的对话框将在组件接口上进行操作,而不是试图找出传入的实际实现是INode
。这将使对话框和节点实现之间的映射更为灵活。对话框可以通过验证它是否为对话框感兴趣的每个接口返回有效对象来快速确定它是否具有“兼容”对象。
我认为在这种情况下,createEditDialog
内部的剧组并不是一件坏事即使你放弃编译时间检查。如果节点的类型在运行时没有更改,则可以使用模板而不是抽象的INode
-类。
否则,您提出的解决方案就是我所想到的解决方案。但是,我会将方法重命名为“getSelectedNodeDialogFactory
”(我知道,长名称),这样很明显返回的工厂是特定于该节点的。是否有其他对话需要知道对象的具体类型? createAddDialog
是否需要父代或前代节点,也许?这些都可以在工厂选择节点类中进行。