在C ++中有一件事让我很长时间以来一直感到不舒服,因为老实说我不知道怎么做,尽管听起来很简单:
如何在C ++中正确实现Factory方法?
目标:允许客户端使用工厂方法而不是对象的构造函数实例化某些对象,而不会造成不可接受的后果和性能损失。
“工厂方法模式”是指对象内部的静态工厂方法或另一个类中定义的方法或全局函数。通常只是“将类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的两个不透明的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中,对于上面的代码,不再可复制不再是问题)
结论:通过返回对象建立工厂确实是某些情况下的解决方案(例如前面提到的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
或引用成员变量, - 将参数传递给基类构造函数和成员对象构造函数。
也许还有一些我现在无法想到的缺点,而且由于上述要点已经说服了我,我什至没有特别的义务。
因此:甚至没有一个实现工厂的好的通用解决方案。
结论:
我们想要一种对象实例化的方法,该方法将:
- 允许统一实例化,而不考虑分配,
- 为构造方法赋予不同的有意义的名称(因此不依赖于参数的重载),
- 不会带来明显的性能下降,最好不会造成重大的代码膨胀,尤其是在客户端,
- 通用的,如:可能被引入任何课堂。
我相信我已经证明我提到的方式不能满足这些要求。
有什么提示吗?请为我提供解决方案,我不想认为这种语言不会允许我正确地实现这种琐碎的概念。
delete
时候。只要调用者获得了指针的所有权(请参阅:负责在适当时删除指针),这些方法就可以很好地工作(源代码是文档;-))。
unique_ptr<T>
而不是使其非常明确T*
。