在 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 和位域),语义上完全不同的类型(因此 - 在理论上 - 有效重载)但实际上结果是同样的事情——比如无符号整数或空指针。
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++ 变得复杂,但也常常使它变得强大。另外,我相信存在一些不允许动态分配的目标(关键字:嵌入式)。这并不意味着这些平台的用户喜欢编写干净的 OOP。
无论如何,抛开哲学:在一般情况下,我不想强迫工厂的用户被限制为动态分配。
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++ 标准已经足够仁慈了,编译器厂商不会指定何时就地创建对象,返回一个对象时何时复制它。 C++ 中按值的临时对象。因此,如果 Foo 的复制成本很高,那么这种方法是有风险的。
如果 Foo 根本不可复制怎么办?嗯,呵呵。(请注意,在保证复制省略的 C++17 中,对于上面的代码,不可复制不再是问题)
结论:通过返回对象来制造工厂确实是某些情况下的解决方案(例如前面提到的二维向量),但仍然不是构造函数的一般替代品。
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
或引用成员变量, - 将参数传递给基类构造函数和成员对象构造函数。
可能还有一些我现在无法想到的缺点,而且我什至不觉得特别有义务,因为上述要点已经说服了我。
所以:甚至没有一个很好的通用解决方案来实现工厂。
结论:
我们希望有一种对象实例化的方式,它会:
- 无论分配如何,都允许统一实例化,
- 为构造方法赋予不同的、有意义的名称(因此不依赖于参数重载),
- 不会引入显着的性能损失,最好是显着的代码膨胀损失,尤其是在客户端,
- 是一般的,如:可能被引入任何类。
我相信我已经证明我提到的方法不能满足这些要求。
有什么提示吗?请给我一个解决方案,我不想认为这种语言不允许我正确实现这样一个微不足道的概念。