c++中有一件事一直让我感到不舒服,因为我真的不知道该怎么做,尽管它听起来很简单:
我如何在c++中正确地实现工厂方法?
目标:允许客户端使用工厂方法而不是对象的构造函数实例化一些对象,而不会造成不可接受的后果和性能损失。
通过“工厂方法模式”,我指的是对象中的静态工厂方法或定义在另一个类中的方法,或全局函数。只是一般的“将类X实例化的正常方式重定向到构造函数以外的任何地方的概念”。
让我粗略地看一下我想到的一些可能的答案。
0)不要创建工厂,而是创建构造函数。
这听起来不错(实际上通常是最好的解决方案),但不是万能的补救措施。首先,在某些情况下,对象构造是一项非常复杂的任务,需要将其提取到另一个类。但即使把这个事实放在一边,即使对于简单的对象,只使用构造函数通常也不行。
我所知道的最简单的例子是2-D Vector类。如此简单,却又棘手。我想用笛卡尔坐标和极坐标来构造它。显然,我不能:
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的两个不透明的类型定义重载(比如不相关域的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) Return-by-value
好的,我们知道当我们需要动态分配时,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中有保证的复制省略,对于上面的代码来说,不可复制不再是问题)
结论:通过返回对象来创建工厂对于某些情况(例如前面提到的2-D向量)确实是一种解决方案,但仍然不是构造函数的一般替代品。
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或引用成员变量, 将参数传递给基类构造函数和成员对象构造函数。
可能还会有一些我现在想不到的缺点,我甚至不觉得有什么特别的义务,因为上面的要点已经说服了我。
所以:对于实现工厂来说,甚至还没有一个好的通用解决方案。
结论:
我们希望有一种对象实例化的方式:
无论分配如何,都允许统一的实例化, 给构造方法取不同的、有意义的名字(这样就不依赖于参数重载), 不要带来严重的性能损失,最好是严重的代码膨胀,尤其是在客户端, 是一般的,例如:可以为任何类引入。
我相信我已经证明了我所提到的方法并不能满足这些要求。
有提示吗?请给我一个解决方案,我不想认为这个语言不能让我正确地实现这样一个微不足道的概念。