How to implement the factory method pattern in C++ correctly
C++中有一件事情让我感到很不舒服,因为我真的不知道怎么做,尽管听起来很简单:
如何正确实现C++中的工厂方法?目标:使客户机能够使用工厂方法而不是对象的构造函数来实例化一些对象,而不会造成不可接受的后果和性能损失。
"工厂方法模式"是指对象内的静态工厂方法或在另一个类中定义的方法,或全局函数。一般来说,"将类X的正常实例化方法重定向到构造函数之外的任何地方的概念"。
让我浏览一下我想到的一些可能的答案。
0)不做工厂,做施工员。这听起来不错(而且往往是最好的解决方案),但不是一般的补救办法。首先,有些情况下,对象构造是一个足够复杂的任务,足以证明将其提取到另一个类中是正确的。但即使将这一事实放在一边,即使对于仅使用构造函数的简单对象,通常也做不到。
我知道的最简单的例子是二维向量类。这么简单,但又很棘手。我想能够用笛卡尔坐标和极坐标来构造它。显然,我不能:
1 2 3 4 5 | struct Vec2 { Vec2(float x, float y); Vec2(float angle, float magnitude); // not a valid overload! // ... }; |
我的自然思维方式是:
1 2 3 4 5 | struct Vec2 { static Vec2 fromLinear(float x, float y); static Vec2 fromPolar(float angle, float magnitude); // ... }; |
这将导致我使用静态工厂方法,而不是构造函数…这实质上意味着我正在以某种方式实现工厂模式("类成为它自己的工厂")。这看起来不错(并且适合这个特定的情况),但在某些情况下失败了,我将在第2点中描述。继续阅读。
另一种情况是:试图通过某些API的两个不透明的typedef(例如不相关域的guid或guid和bitfield)进行重载,类型在语义上完全不同(理论上是有效的重载),但事实证明它们是相同的,如无符号int或void指针。
1)Java方式Java之所以简单,是因为我们只有动态分配的对象。制造工厂就像:
1 2 3 4 5 6 7 8 | 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++中,翻译成:
1 2 3 4 5 6 | class FooFactory { public: Foo* createFooInSomeWay() { return new Foo(some, args); } }; |
Cool?的确如此。但是-这迫使用户只使用动态分配。静态分配是C++复杂的原因,也是经常使它强大的原因。另外,我认为存在一些不允许动态分配的目标(关键字:embedded)。这并不意味着这些平台的用户喜欢编写干净的OOP。
不管怎样,哲学除外:在一般情况下,我不想强迫工厂的用户受到动态分配的限制。
2)按值返回好的,所以我们知道1)当我们需要动态分配时很酷。为什么不在上面添加静态分配呢?
1 2 3 4 5 6 7 8 9 | class FooFactory { public: Foo* createFooInSomeWay() { return new Foo(some, args); } Foo createFooInSomeWay() { return Foo(some, args); } }; |
什么?返回类型不能超载?哦,当然我们不能。所以让我们更改方法名来反映这一点。是的,我写了上面的无效代码示例,只是为了强调我有多不喜欢更改方法名的需要,例如,因为我们现在不能正确地实现语言不可知的工厂设计,因为我们必须更改名称-而且此代码的每个用户都需要记住实现与特定。
1 2 3 4 5 6 7 8 9 | class FooFactory { public: Foo* createDynamicFooInSomeWay() { return new Foo(some, args); } Foo createFooObjectInSomeWay() { return Foo(some, args); } }; |
好啊。。。我们找到了。它很难看,因为我们需要更改方法名。这是不完美的,因为我们需要写两次相同的代码。但一旦完成,它就会起作用。对吗?
嗯,通常是这样。但有时不是。在创建FoO时,我们实际上依赖于编译器对我们进行返回值优化,因为C++标准对编译器厂商来说是足够仁慈的,而不必指定对象在什么时候被创建,以及何时在C++中以值返回临时对象时将被复制。因此,如果FOO的复制成本很高,这种方法是有风险的。
如果foo根本不可复制怎么办?哦,多哈。(请注意,在C++ 17中,保证复制删除,不可复制对于上面的代码不再是问题)
结论:对于某些情况(如前面提到的二维向量),通过返回对象来创建工厂确实是一种解决方案,但仍然不是一般的构造函数替换。
3)两相施工另一个可能会被人想到的事情是分离对象分配和初始化的问题。这通常会导致如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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()); // ... } |
人们可能会认为它很有魅力。我们在代码中支付的唯一价格…
既然我写了这一切,把它留作最后一个,我也一定不喜欢它。为什么?
首先…我真的不喜欢两阶段结构的概念,当我使用它的时候我感到内疚。如果我设计对象时断言"如果它存在,它就处于有效状态",那么我觉得我的代码更安全,更不容易出错。我喜欢那样。<
First of all, there are cases when
object construction is a task complex
enough to justify its extraction to
another class.
我认为这一点是错误的。复杂性并不重要。相关性就是它的作用。如果一个对象可以在一个步骤中构造(不像在构建器模式中那样),那么构造器就是正确的地方。如果您确实需要另一个类来执行该作业,那么它应该是一个从构造函数使用的助手类。
1 2 | Vec2(float x, float y); Vec2(float angle, float magnitude); // not a valid overload! |
有一个简单的解决方法:
1 2 3 4 5 6 7 8 9 10 | struct Cartesian { inline Cartesian(float x, float y): x(x), y(y) {} float x, y; }; struct Polar { inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {} float angle, magnitude; }; Vec2(const Cartesian &cartesian); Vec2(const Polar &polar); |
唯一的缺点是它看起来有点冗长:
1 | Vec2 v2(Vec2::Cartesian(3.0f, 4.0f)); |
但好的是,您可以立即看到正在使用的坐标类型,同时不必担心复制。如果您想要复制,而且复制成本很高(当然,通过分析可以证明这一点),那么您可能希望使用类似qt的共享类来避免复制开销。
至于分配类型,使用工厂模式的主要原因通常是多态性。构造器不能是虚拟的,即使可以,也没有什么意义。使用静态或堆栈分配时,不能以多态方式创建对象,因为编译器需要知道确切的大小。所以它只适用于指针和引用。从工厂返回一个引用也不起作用,因为虽然技术上的对象可以通过引用来删除,但是它可能相当混乱和容易出错,参见返回C++引用变量的做法吗?例如。所以指针是唯一剩下的东西,也包括智能指针。换句话说,工厂在与动态分配一起使用时最有用,因此您可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Abstract { public: virtual void do() = 0; }; class Factory { public: Abstract *create(); }; Factory f; Abstract *a = f.create(); a->do(); |
在其他情况下,工厂只是帮助解决一些小问题,比如那些你提到过的过载问题。如果能以统一的方式使用它们,那就太好了,但这可能是不可能的,这并不会造成太大的伤害。
简单工厂示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Factory returns object and ownership // Caller responsible for deletion. #include <memory> class FactoryReleaseOwnership{ public: std::unique_ptr<Foo> createFooInSomeWay(){ return std::unique_ptr<Foo>(new Foo(some, args)); } }; // Factory retains object ownership // Thus returning a reference. #include <boost/ptr_container/ptr_vector.hpp> class FactoryRetainOwnership{ boost::ptr_vector<Foo> myFoo; public: Foo& createFooInSomeWay(){ // Must take care that factory last longer than all references. // Could make myFoo static so it last as long as the application. myFoo.push_back(new Foo(some, args)); return myFoo.back(); } }; |
你有没有想过不使用工厂,而是好好利用类型系统?我可以想到两种不同的方法来做这种事情:
选项1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct linear { linear(float x, float y) : x_(x), y_(y){} float x_; float y_; }; struct polar { polar(float angle, float magnitude) : angle_(angle), magnitude_(magnitude) {} float angle_; float magnitude_; }; struct Vec2 { explicit Vec2(const linear &l) { /* ... */ } explicit Vec2(const polar &p) { /* ... */ } }; |
它可以让你写一些东西,比如:
1 | Vec2 v(linear(1.0, 2.0)); |
选项2:
您可以像STL对迭代器等使用"标记"。例如:
1 2 3 4 5 6 7 | struct linear_coord_tag linear_coord {}; // declare type and a global struct polar_coord_tag polar_coord {}; struct Vec2 { Vec2(float x, float y, const linear_coord_tag &) { /* ... */ } Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ } }; |
第二种方法允许您编写如下所示的代码:
1 | Vec2 v(1.0, 2.0, linear_coord); |
它也很好,表达能力也很强,同时允许您为每个构造函数拥有独特的原型。
您可以在http://www.codeproject.com/articles/363338/factory-pattern-in-cplusplus中阅读一个非常好的解决方案。
最好的解决方案是"评论和讨论",参见"不需要静态创建方法"。
基于这个想法,我做了一个工厂。请注意,我使用的是qt,但是您可以更改qmap和qstring作为std等价物。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #ifndef FACTORY_H #define FACTORY_H #include <QMap> #include <QString> template <typename T> class Factory { public: template <typename TDerived> void registerType(QString name) { static_assert(std::is_base_of<T, TDerived>::value,"Factory::registerType doesn't accept this type because doesn't derive from base class"); _createFuncs[name] = &createFunc<TDerived>; } T* create(QString name) { typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name); if (it != _createFuncs.end()) { return it.value()(); } return nullptr; } private: template <typename TDerived> static T* createFunc() { return new TDerived(); } typedef T* (*PCreateFunc)(); QMap<QString,PCreateFunc> _createFuncs; }; #endif // FACTORY_H |
样品使用情况:
1 2 3 4 5 6 7 | Factory<BaseClass> f; f.registerType<Descendant1>("Descendant1"); f.registerType<Descendant2>("Descendant2"); Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1")); Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2")); BaseClass *b1 = f.create("Descendant1"); BaseClass *b2 = f.create("Descendant2"); |
我主要同意所接受的答案,但是有一个C++ 11选项,在现有的答案中没有被覆盖:
- 按值返回工厂方法结果,以及
- 提供廉价的移动构造函数。
例子:
1 2 3 4 5 6 7 8 | struct sandwich { // Factory methods. static sandwich ham(); static sandwich spam(); // Move constructor. sandwich(sandwich &&); // etc. }; |
然后可以在堆栈上构造对象:
1 | sandwich mine{sandwich::ham()}; |
作为其他事物的子对象:
1 | auto lunch = std::make_pair(sandwich::spam(), apple{}); |
或动态分配:
1 | auto ptr = std::make_shared<sandwich>(sandwich::ham()); |
我什么时候可以用这个?
如果在公共构造函数上,不经过初步计算就无法为所有类成员提供有意义的初始化器,那么我可以将该构造函数转换为静态方法。静态方法执行初步计算,然后通过一个私有构造函数返回一个值结果,该构造函数只执行成员初始化。
我之所以说"可能",是因为它取决于哪种方法可以提供最清晰的代码,而不会产生不必要的效率低下。
Loki既有工厂方法,也有抽象工厂。两种都被广泛记录在现代C++设计中,由Andei Alexandrescu。factory方法可能更接近于您所追求的,尽管它仍然有点不同(至少在内存可用的情况下,它要求您先注册一个类型,然后factory才能创建该类型的对象)。
我不想回答我所有的问题,因为我认为这太宽泛了。只是几条注释:
there are cases when object construction is a task complex enough to justify its extraction to another class.
这个类实际上是一个构建者,而不是一个工厂。
In the general case, I don't want to force the users of the factory to be restrained to dynamic allocation.
然后你可以让你的工厂用一个智能指针来封装它。我相信这样你就可以吃蛋糕了。
这也消除了与按值返回相关的问题。
Conclusion: Making a factory by returning an object is indeed a solution for some cases (such as the 2-D vector previously mentioned), but still not a general replacement for constructors.
的确。所有设计模式都有其(语言特定的)约束和缺点。建议仅在它们帮助您解决问题时使用它们,而不是为了它们自己。
如果你是经过"完美"工厂实施的,那么,祝你好运。
这是我的C++ 11风格的解决方案。参数"base"用于所有子类的基类。创建者是用于创建子类实例的std::function对象,可能是与子类"static member function"create(某些参数)的绑定。这也许不完美,但对我有用。这是一种"通用"的解决方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | template <class base, class... params> class factory { public: factory() {} factory(const factory &) = delete; factory &operator=(const factory &) = delete; auto create(const std::string name, params... args) { auto key = your_hash_func(name.c_str(), name.size()); return std::move(create(key, args...)); } auto create(key_t key, params... args) { std::unique_ptr<base> obj{creators_[key](args...)}; return obj; } void register_creator(const std::string name, std::function<base *(params...)> &&creator) { auto key = your_hash_func(name.c_str(), name.size()); creators_[key] = std::move(creator); } protected: std::unordered_map<key_t, std::function<base *(params...)>> creators_; }; |
使用示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | class base { public: base(int val) : val_(val) {} virtual ~base() { std::cout <<"base destroyed "; } protected: int val_ = 0; }; class foo : public base { public: foo(int val) : base(val) { std::cout <<"foo" << val <<" "; } static foo *create(int val) { return new foo(val); } virtual ~foo() { std::cout <<"foo destroyed "; } }; class bar : public base { public: bar(int val) : base(val) { std::cout <<"bar" << val <<" "; } static bar *create(int val) { return new bar(val); } virtual ~bar() { std::cout <<"bar destroyed "; } }; int main() { common::factory<base, int> factory; auto foo_creator = std::bind(&foo::create, std::placeholders::_1); auto bar_creator = std::bind(&bar::create, std::placeholders::_1); factory.register_creator("foo", foo_creator); factory.register_creator("bar", bar_creator); { auto foo_obj = std::move(factory.create("foo", 80)); foo_obj.reset(); } { auto bar_obj = std::move(factory.create("bar", 90)); bar_obj.reset(); } } |
我知道这个问题3年前就已经被回答过了,但这可能是你想要的。
几周前,谷歌发布了一个库,允许轻松灵活的动态对象分配。这里是:http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html
工厂模式
1 2 3 4 5 6 | class Point { public: static Point Cartesian(double x, double y); private: }; |
如果编译器不支持返回值优化,那么放弃它,它可能根本不包含太多优化…