如何在C ++中正确实现工厂方法模式

C ++中有这样一件事情,使我长时间感到不舒服,因为我真的不知道该怎么做,尽pipe听起来很简单:

如何在C ++中正确实现工厂方法?

目标:使客户可以使用工厂方法而不是对象的构造函数实例化一些对象,而不会造成不可接受的后果和性能问题。

“工厂方法模式”是指对象内部的静态工厂方法或其他类中定义的方法或全局函数。 一般来说,“把X类实例化的正常方式redirect到构造函数以外的任何地方”的概念。

让我浏览一下我想到的一些可能的答案。


0)不要制造工厂,build造者。

这听起来不错(通常是最好的解决scheme),但不是一般的补救措施。 首先,有些情况下,对象build设是一个任务复杂到足以certificate其提取到另一个类。 但是即使把这个事实放在一边,即使对于使用构造函数的简单对象也是不行的。

我所知道的最简单的例子是一个2Dvector类。 这么简单,但棘手。 我希望能够从笛卡儿坐标和极坐标两方面构造它。 显然,我不能这样做:

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); // ... }; 

其中,而不是构造函数,导致我使用静态工厂方法…这基本上意味着我正在实施工厂模式,以某种方式(“class级成为自己的工厂”)。 这看起来不错(并且适合这种特殊情况),但在某些情况下会失败,我将在第2点中描述。请继续阅读。

另一种情况:试图通过一些API的两个不透明types定义(例如不相关域的GUID,或一个GUID和一个位域)重载,types在语义上完全不同(理论上是有效的重载),但实际上却是同样的事情 – 像无符号整数或无效指针。


1)Java的方式

Java很简单,因为我们只有dynamic分配的对象。 制造工厂和以下一样微不足道:

 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); } }; 

凉? 通常的确如此。 但是,这迫使用户只使用dynamic分配。 静态分配是C ++的复杂性,但也常常使其强大。 另外,我认为存在一些不允许dynamic分配的目标(关键字:embedded式)。 而这并不意味着这些平台的用户喜欢写干净的OOP。

无论如何,把哲学放在一边:一般情况下,我不想强​​迫工厂的用户被dynamic分配。


2)按价值回报

好,所以我们知道1)当我们想要dynamic分配的时候很酷。 为什么我们不添加静态分配?

 class FooFactory { public: Foo* createFooInSomeWay() { return new Foo(some, args); } Foo createFooInSomeWay() { return Foo(some, args); } }; 

什么? 我们不能通过返回types来重载? 哦,当然,我们不能。 所以让我们改变方法名称来反映这一点。 是的,我已经写了上面的无效代码示例,只是为了强调我不喜欢改变方法名称的需要,例如因为我们现在不能实现一个语言不可知的工厂devise,因为我们必须改变名称这个代码的每个用户都需要记住实现与规范的区别。

 class FooFactory { public: Foo* createDynamicFooInSomeWay() { return new Foo(some, args); } Foo createFooObjectInSomeWay() { return Foo(some, args); } }; 

好的…我们有。 这很丑陋,因为我们需要改变方法名称。 这是不完美的,因为我们需要两次编写相同的代码。 但一旦完成,它的工作。 对?

那么,通常。 但有时候并不是这样。 在创buildFoo时,我们实际上依赖于编译器为我们做了返回值优化,因为C ++标准对于编译器供应商来说是不够的,不能指定对象在什么时候创build以及何时在返回时复制C ++中的临时对象。 所以如果Foo的拷贝成本很高,这种方法是有风险的。

而如果Foo根本不可复制呢? 那么,呃。 ( 请注意,在C ++ 17中,保证副本不会被复制,对于上面的代码已经不是问题了

结论:通过返回对象来创build工厂对于某些情况(例如前面提到的2-D向量)来说确实是一个解决scheme,但仍然不是构造函数的一般替代品。


3)两阶段build设

有人可能提出的另一件事是分离对象分配和初始化的问题。 这通常会导致这样的代码:

 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()); // ... } 

人们可能会认为它像一个魅力。 我们在代码中唯一支付的价格…

既然我已经写完所有这些,并且把它作为最后一个,我也不喜欢它。 :)为什么?

首先,我真诚地不喜欢两阶段build设的概念,当我使用它时,我感到内疚。 如果我devise我的对象的断言,“如果它存在,它是在有效的状态”,我觉得我的代码更安全,更不容易出错。 我喜欢这样。

不得不放弃这个惯例,改变我的对象的devise,只是为了制造它的工厂的目的是好的,笨拙的。

我知道上面不会说服很多人,所以让我来说一些更加可靠的论点。 使用两相结构,您不能:

  • 初始化const或引用成员variables,
  • 将parameter passing给基类构造函数和成员对象构造函数。

而且可能还有一些我现在想不到的缺点,从上面的要点来说,我甚至没有特别的感觉。

所以:甚至没有接近一个很好的实施工厂的一般解决scheme。


结论:

我们想要有一个对象实例化的方法,它会:

  • 允许统一实例化,不pipe分配,
  • 给build筑方法赋予不同的,有意义的名称(因此不依赖于过度重载),
  • 不会引入显着的性能下降,最好是严重的代码膨胀,特别是在客户端,
  • 一般,如:可能为任何阶级引入。

我相信我已经certificate,我提到的方式不符合这些要求。

任何提示? 请给我提供一个解决scheme,我不想认为这种语言不会让我正确实施这样一个小概念。

首先,有些情况下,对象build设是一个任务复杂到足以certificate其提取到另一个类。

我相信这一点是不正确的。 复杂性并不重要。 相关性是什么。 如果一个对象可以在一个步骤中构build(不像构build器模式中那样),构造器是正确的。 如果你真的需要另一个类来执行这个工作,那么它应该是一个从构造函数中使用的帮助类。

 Vec2(float x, float y); Vec2(float angle, float magnitude); // not a valid overload! 

有一个简单的解决方法:

 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); 

唯一的缺点是它看起来有点冗长:

 Vec2 v2(Vec2::Cartesian(3.0f, 4.0f)); 

但好处是,你可以立即看到你使用的坐标types,同时你不必担心复制。 如果你想复制,并且它是昂贵的(当然,通过分析certificate),你可能希望使用类似Qt的共享类来避免复制开销。

至于分配types,使用工厂模式的主要原因通常是多态。 build设者不可能是虚拟的,即使可能,也没有什么意义。 当使用静态或堆栈分配时,您不能以多态的方式创build对象,因为编译器需要知道确切的大小。 所以它只适用于指针和引用。 并且从工厂返回引用也不起作用,因为虽然从技术上讲可以通过引用删除一个对象,但它可能相当混乱,容易出错,请参阅返回C ++引用variables的做法,邪恶吗? 例如。 所以指针是唯一剩下的东西,也包括智能指针。 换句话说,使用dynamic分配工厂是最有用的,所以你可以做这样的事情:

 class Abstract { public: virtual void do() = 0; }; class Factory { public: Abstract *create(); }; Factory f; Abstract *a = f.create(); a->do(); 

在其他情况下,工厂只是帮助解决你提到的超载问题。 如果有可能以统一的方式使用它们,那将是很好的,但是它可能不会造成太大的伤害。

简单的工厂例子:

 // 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(); } }; 

你有没有想过不使用工厂,而是很好地使用types系统? 我可以想到两种不同的方法来做这种事情:

选项1:

 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) { /* ... */ } }; 

这可以让你写下如下的东西:

 Vec2 v(linear(1.0, 2.0)); 

选项2:

你可以像STL一样使用“标签”来处理迭代器等。 例如:

 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 &) { /* ... */ } }; 

第二种方法可以让你编写如下所示的代码:

 Vec2 v(1.0, 2.0, linear_coord); 

这也是很好的和performance力,同时让你有每个构造函数独特的原型。

你可以阅读一个非常好的解决scheme: http : //www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

最好的解决scheme是在“评论和讨论”上,请参阅“不需要静态创build方法”。

从这个想法,我做了一个工厂。 请注意,我正在使用Qt,但是您可以将QMap和QString更改为std等效项。

 #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 

示例用法:

 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"); 

Loki拥有工厂方法和抽象工厂 。 在Andei Alexandrescu的“ 现代C ++devise”中都广泛地logging了这两者。 工厂方法可能更接近你似乎以后,虽然它仍然有点不同(至less如果内存服务,它需要你注册一个types之前,工厂可以创build该types的对象)。

我大部分都同意接受的答案,但有一个C ++ 11选项尚未被现有答案所覆盖:

  • 按值返回工厂方法结果
  • 提供一个便宜的移动构造函数

例:

 struct sandwich { // Factory methods. static sandwich ham(); static sandwich spam(); // Move constructor. sandwich(sandwich &&); // etc. }; 

然后你可以在堆栈上构build对象:

 sandwich mine{sandwich::ham()}; 

作为其他事物的子对象:

 auto lunch = std::make_pair(sandwich::spam(), apple{}); 

或者dynamic分配:

 auto ptr = std::make_shared<sandwich>(sandwich::ham()); 

我什么时候可以用这个?

如果在一个公共的构造函数中,没有一些初步的计算就不可能为所有的类成员提供有意义的初始化,那么我可能会将这个构造函数转换为静态方法。 静态方法执行初步计算,然后通过私有构造函数返回值结果,该构造函数只是进行成员初始化。

我说' 可能 ',因为它取决于哪种方法提供了最清晰的代码,而没有不必要的低效率。

我不想回答所有的问题,因为我认为这个问题太广泛了。 只是几个注意事项:

有些情况下,对象build设是一个任务复杂到足以certificate其提取到另一个类。

那个class其实是一个build筑师 ,而不是工厂。

在一般情况下,我不想强​​迫工厂的用户被dynamic分配。

然后你可以让你的工厂封装在一个智能指针。 我相信这样你可以吃你的蛋糕,也可以吃。

这也消除了与价值回报有关的问题。

结论:通过返回对象来创build工厂确实是某些情况下的解决scheme(例如前面提到的2-D向量),但仍然不是构造函数的一般replace。

确实。 所有的devise模式都有其特定的语言限制和缺陷。 build议只有在他们帮你解决问题时才使用它们,而不是为了自己的利益。

如果你是在“完美”的工厂实施之后,那么祝你好运。

工厂模式

 class Point { public: static Point Cartesian(double x, double y); private: }; 

如果你的编译器不支持返回值优化,那么它可能并不包含太多的优化…

我知道这个问题已经在三年前回答了,但这可能是你正在寻找的东西。

谷歌几周前发布了一个允许简单而灵活的dynamic对象分配的库。 这里是: http : //google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

这是我的C ++ 11风格的解决scheme。 参数“base”是所有子类的基类。 创造者,是std ::函数对象来创build子类实例,可能是一个绑定到你的子类的静态成员函数'创build(一些参数)'。 这可能不完美,但为我工作。 这是一个'一般'的解决scheme。

 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_; }; 

使用示例。

 class base { public: base(int val) : val_(val) {} virtual ~base() { std::cout << "base destroyed\n"; } protected: int val_ = 0; }; class foo : public base { public: foo(int val) : base(val) { std::cout << "foo " << val << " \n"; } static foo *create(int val) { return new foo(val); } virtual ~foo() { std::cout << "foo destroyed\n"; } }; class bar : public base { public: bar(int val) : base(val) { std::cout << "bar " << val << "\n"; } static bar *create(int val) { return new bar(val); } virtual ~bar() { std::cout << "bar destroyed\n"; } }; 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(); } }