在C++中有一件事情让我感到不舒服已经很长时间了,因为我实际上不知道如何做到它,即使它听起来很简单:
如何正确实现C++中的工厂方法?
目标:使客户端能够使用工厂方法来实例化某个对象,而不是使用对象的构造函数,同时避免不可接受的后果和性能损失。
我所说的“工厂方法模式”包括对象内的静态工厂方法、在另一个类中定义的方法或全局函数。总之,“将类X的正常实例化方式重定向到除构造函数以外的任何地方”的概念。
让我快速浏览一些我考虑过的可能答案。
0)不要制造工厂,制造构造函数。
这听起来不错(确实通常是最好的解决方案),但不是通用的解决办法。首先,有些情况下,对象构造是足够复杂的任务,需要将其提取到另一个类中进行。但即使将这个事实放在一边,对于只使用构造函数的简单对象,也经常行不通。
我知道的最简单的例子是一个二维向量类。非常简单,却很棘手。我希望能够同时从笛卡尔坐标和极坐标构造它。显然,我不能这样做:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
我的自然思维方式是:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
我将使用静态工厂方法来替代构造函数,这本质上意味着我正在实现工厂模式(“类成为自己的工厂”)。这看起来很不错(并且适合这种特定情况),但在某些情况下会失败,我将在第2点中描述。请继续阅读。
另一种情况是尝试通过两个不透明的API typedef进行重载(例如不相关领域的GUID或GUID和位字段),这些类型在语义上完全不同(因此 - 在理论上 - 是有效的重载),但实际上却是相同的东西 - 就像无符号整数或void指针。
1)Java方式
对于Java来说很简单,因为我们只有动态分配的对象。制作一个工厂就像这样简单:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
在C++中,这可以翻译为:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
很酷?确实如此。但这迫使用户仅使用动态分配。静态分配是使C++复杂的原因,但也经常使其强大。此外,我相信存在一些目标(关键字:嵌入式)不允许动态分配。这并不意味着这些平台的用户喜欢编写干净的面向对象程序。
无论如何,撇开哲学不谈:在一般情况下,我不想强制工厂的用户受到动态分配的限制。
2) 按值返回
好的,我们知道当我们想要动态分配时,1)很酷。为什么我们不在此基础上添加静态分配呢?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
什么?我们不能通过返回类型进行重载吗?当然不能。那么让我们更改方法名称以反映这一点。是的,我已经编写了上面的无效代码示例,只是为了强调我有多么不喜欢需要更改方法名称,例如因为我们现在无法正确实现与语言无关的工厂设计,因为我们必须更改名称-并且该代码的每个用户都需要记住从规范中实现的差异。
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
好的,内容翻译完成。由于我们需要更改方法名称,因此它看起来很丑陋。由于我们需要两次编写相同的代码,所以它并不完美。但是一旦完成,它就可以工作了。对吧?
通常是这样的。但有时会失败。在创建Foo时,实际上我们依赖于编译器为我们执行返回值优化,因为C++标准足够仁慈,不会指定在返回按值临时对象时何时将对象原地创建和何时复制该对象。因此,如果复制Foo的成本很高,这种方法是有风险的。
如果Foo根本无法复制怎么办?那就糟糕了。(注意,在具有保证复制省略的C++17中,上述代码中的无法复制已经不再是问题)
结论:通过返回对象来创建工厂确实是一种解决某些情况(例如先前提到的2D向量)的方法,但仍不能普遍替代构造函数。
3)两阶段构建
另一种可能被想出的方法是将对象分配和初始化的问题分离。这通常会导致以下代码:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
有些人可能会觉得这很完美。我们在代码中需要付出的唯一代价是......
既然我已经写了所有的内容并将它们留到最后,那么我肯定也不喜欢它。为什么呢?
首先......我真心不喜欢两阶段构造法,并且当我使用它时会感到内疚。如果我设计我的对象并断言“如果存在,则处于有效状态”,那么我感觉我的代码更加安全,更少出错。我喜欢这种方式。
必须放弃这个约定并且改变对象的设计只是为了让它成为一个工厂,这是笨拙的。
我知道上面的观点不会说服很多人,所以让我再提供一些更实际的论据。使用两阶段构造法,您无法:
初始化const或引用成员变量,
向基类构造函数和成员对象构造函数传递参数。
也许还有一些缺点我现在想不到,而且我甚至不觉得有必要去想,因为上述要点已经让我相信这并不是实现工厂的好通用解决方案。
所以:离实现工厂的一个良好的通用解决方案还有很远的路要走。
结论:
我们希望有一种对象实例化的方式:
不论分配如何,都可以进行统一的实例化;
给构造方法命名不同并且有意义(因此不依赖于按参数重载);
不引入显著的性能损失和代码膨胀损失,尤其是在客户端;
是通用的,即适用于任何类。
我相信我已经证明了我提到的方法都无法满足这些要求。
有什么提示吗?请给我提供一个解决方案,我不想认为这种语言不能让我正确地实现这样一个微不足道的概念。
delete
它的情况下才可以这样做。只要“记录”(源代码是文档;-))调用者获取指针的所有权(即负责在适当时删除它),这种方法就完全没问题。 - Boris Dalsteinunique_ptr<T>
而不是T*
来使其更加明确。 - Kos