Factory Pattern in C++ — doing this correctly?
我对"设计模式"比较陌生,因为它们在形式上被提到。我已经不是一个专业人士很长时间了,所以我对这个很陌生。
我们有一个纯虚拟接口基类。显然,这个接口类提供了它的派生子类应该做什么功能的定义。软件中的当前使用和情况决定了我们要使用的派生子类型,因此我建议创建一个包装器,它将传达我们想要的派生子类型,并返回指向新派生对象的基指针。据我所知,这个包装器是一个工厂。
我的一个同事在基类中创建了一个静态函数来充当工厂。这给我带来麻烦有两个原因。首先,它似乎破坏了基类的接口性质。我觉得界面本身需要了解从中衍生出来的孩子们是不对的。
其次,当我尝试在两个不同的Qt项目中重用基类时,它会导致更多的问题。在一个项目中,我正在实现第一个(并且可能只有这一个类的真正实现)。尽管我想对另外两个具有几个不同派生类的特性使用相同的方法)派生类,第二个是实际应用程序,我的代码最终将在其中使用。我的同事创建了一个派生类,在我编写自己的部分代码时充当实际应用程序的测试人员。这意味着我必须将他的头文件和cpp文件添加到我的项目中,这似乎是错误的,因为我在实现我的部分时甚至没有使用他的项目代码(但完成后他将使用我的代码)。
我是否正确地认为工厂确实需要是一个围绕基类的包装器,而不是作为工厂的基础?
- @如果你看看我以前的一些评论,我倾向于同意。但是,当我有关于代码体系结构的问题时,我必须尽我所能地使用社区的术语来表达这个问题。似乎大家都一致认为我的想法是正确的:按目前的方式做是有限的,而且会引起问题。通过使用"工厂"术语,我能够表达我的问题。你看到我这样做的好处了吗?
- 我不是想判断你在这里问了些什么。我只是做了一个笼统的陈述。我现在明白这似乎是对你问题的攻击。很抱歉。
- @SBI我想说的是,你的评论让我问,"你认为尝试使用一种技术的常用术语是错误的,还是我不应该使用这种技术?"比这更让我觉得受到了攻击。你对So的回答和评论通常比较简洁,所以当我在寻找适合我正在查找的内容时,我通常希望你尽可能多地分享你的陈述的理由,因为我知道你的个人推理能力很强。因此,我对你的评论发表评论。
- 非常感谢您的反馈!将来我会尽量不把我的答案简明扼要。
您不想将接口类用作工厂类。首先,如果它是一个真正的接口类,就没有实现。第二,如果接口类确实定义了一些实现(除了纯虚拟函数),那么现在使静态工厂方法强制在每次添加子类实现时重新编译基类。
实现工厂模式的最佳方法是将接口类与工厂分离。
下面是一个非常简单(且不完整)的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class MyInterface
{
public:
virtual void MyFunc() = 0;
};
class MyImplementation : public MyInterface
{
public:
virtual void MyFunc() {}
};
class MyFactory
{
public:
static MyInterface* CreateImplementation(...);
}; |
- 完全正确——只是没有理由让CreateImplementation是静态的。它应该是非静态方法或自由函数。
- 好啊。谢谢。这是我的想法。他进来之前我先动身。他必须做的工作越少,他越有可能接受替代方案:)
- +1尽管如此,我还是希望看到CreateImplementation返回智能指针或副本,而不是原始指针。
- 什么t33c说:只是返回一个简单的值而不是指针,并依靠编译器做RVO的性能-除非您有特殊的需要否则,然后使用一个智能指针容器。
- @弗雷德:对。它可以是一个自由函数。我将它留在自己的类中,以表明您不希望它出现在接口定义中。
- @T33C:也正确。我只是举了个很简单的例子。
- @eamon:不,不能按值返回抽象基类;工厂必须返回指针,smart或其他类型。
- 一般情况下,CreateImplementation不能按值返回。由工厂函数创建的对象的运行时类型可能取决于工厂函数的参数(或者如果工厂函数是成员函数,也取决于注入到其调用的对象中的依赖项)。这就是调用工厂函数而不是构造函数的关键所在——您不关心返回什么类。只有在以下两种情况下返回智能指针才是正常的(在DLL中):每个人都使用相同的智能指针用于所有目的;或者您选择一个具有release()函数的智能指针。当心以东十一〔二〕号。
- 魔鬼拥护者:为什么每次添加类时都必须重新编译基类很重要?这比每次添加类时都要求重新编译工厂类更糟糕吗?毕竟,类接口没有改变,所以这不会导致级联其他更新和大量编译时间的减慢。
- @伊蒙:我们为什么要把东西放到课堂上?为什么不让每个应用程序都成为一个巨大的整体实体呢?它不仅仅是编译,它还要求重新链接每个子类,并且在逻辑上,只是将一个方法放在它不在下面的位置。以前的工程师曾在一个项目中做过这样的错误实践,所以最好将工厂方法放在基类之外。
- @eamon:正如zac所说的,在一个不是垃圾的设计中,使用接口的东西可能比使用工厂的要多,因为有些东西会将对象作为参数并对其进行操作,但不会自己创建对象。因此,您可以通过确保只需要接口的东西也不依赖于工厂功能来减少耦合/依赖性。从根本上说,"工厂功能"的概念是有缺陷的。如果接口是一个有价值的抽象,那么可能有不止一种可以想到的方法来创建实现它的对象。
- 感谢您的反馈!更清楚地说:我在头脑风暴,可能在胡说八道(例如,按价值回报的胡说八道)。在接口中包含(静态)工厂方法不需要重新链接子类:实际上,接口只是一个容易记住的名称空间,对吗?所以,这不是简单性和避免大型API和细粒度控制之间的折衷吗?例如,许多接口可以拆分为组件,但在某些情况下,额外的精度不值得如此复杂。
- @eamon:您不会增加API的大小;您正在组织API,使每个接口/实现都有一个用途。当您开始让对象服务于多个目的时,您会增加代码的复杂性(并且通常最终不得不为某些状态制作特殊情况)。
- 但没有涉及对象——毕竟这是一个静态方法。工厂方法很可能与接口的其他代码分离在一个单独的编译单元中——它只是名称间距。而且,让接口作为该接口的工厂方法的名称空间,从而避免名称空间污染似乎很自然。
- @伊蒙:这是来自经验-这样做是不自然的。对代码进行逻辑分组,其他程序员会为此感谢您。试着将它"出于方便"分组,这样你的照片就会挂在墙上,并定期投掷飞镖。
- 因此,有一种观点认为,在一个文件中包含有关抽象基类的所有实现细节是常规的;如果是这样,那么在ABC上使用工厂方法意味着每当添加子类时都要重新编译ABC(如果您的ABC有一个小的实现,这可能很便宜)。此外,Steve建议将工厂作为一个单独的API,以最小化API表面。另一方面,额外的代码文件和额外的名称空间有其自身的内在成本。这就意味着界面的使用越广泛,将工厂分离出来就越有价值。
我不得不同意你的看法。可能面向对象编程最重要的原则之一是对一段代码的范围(无论是方法、类还是名称空间)负有单一责任。在您的例子中,基类用于定义接口。在这个类中添加一个工厂方法,违反了这一原则,打开了一扇通往世界的大门。麻烦。
- 真的很真实。基类只是一个基类,而不是它本身或其后代的工厂。
是的,接口(基类)中的静态工厂方法要求它具有所有可能的实例化知识。这样,您就无法获得工厂方法模式所带来的任何灵活性。
工厂应该是一段独立的代码,由客户机代码用来创建实例。您必须在程序中的某个地方决定要创建什么具体实例。工厂方法允许您避免通过客户机代码进行相同的决策。如果以后您想要更改实现(或者例如,为了测试),您只有一个地方可以编辑:这可能是一个简单的全局更改,通过条件编译(通常是为了测试),甚至通过依赖项注入配置文件。
注意客户机代码如何传递它想要的实现类型:这不是一种重新引入工厂要隐藏的依赖关系的常见方法。
在类中看到工厂成员函数并不少见,但它会让我眼睛出血。它们的使用常常与命名的构造函数习惯用法的功能混合在一起。将创建函数移动到单独的工厂类将为您提供更大的灵活性,并在测试期间交换工厂。
- 如果我的同事愿意的话,我建议在工厂中结合这个习语,但是在不同的代码集中我甚至不使用的模块的耦合对我来说似乎很糟糕。
当接口只是为了隐藏实现细节,并且基本接口的实现只有一个时,可以将它们结合起来。在这种情况下,工厂函数只是实际实现的构造函数的一个新名称。
然而,这种情况很少见。除非显式设计只有一个实现,否则最好假设在某个时间点存在多个实现,如果只是为了测试(如您发现的那样)。
所以通常最好将工厂部件划分为单独的类。
- 如果一个接口只有一个实现,那么您就不需要真正的接口。您可以使用PIMPL模式(尽管过度使用它有其自身的缺点)。