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


329

在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或引用成员变量,
  • 将参数传递给基类构造函数和成员对象构造函数。

也许还有一些我现在无法想到的缺点,而且由于上述要点已经说服了我,我什至没有特别的义务。

因此:甚至没有一个实现工厂的好的通用解决方案。


结论:

我们想要一种对象实例化的方法,该方法将:

  • 允许统一实例化,而不考虑分配,
  • 为构造方法赋予不同的有意义的名称(因此不依赖于参数的重载),
  • 不会带来明显的性能下降,最好不会造成重大的代码膨胀,尤其是在客户端,
  • 通用的,如:可能被引入任何课堂。

我相信我已经证明我提到的方式不能满足这些要求。

有什么提示吗?请为我提供解决方案,我不想认为这种语言不会允许我正确地实现这种琐碎的概念。


7
@Zac,尽管标题非常相似,但实际问题有所不同。
彼得Török

2
重复的很好,但是这个问题的内容本身很有价值。
dmckee ---前主持人小猫,

7
提出此问题两年后,我要补充一点:1)这个问题与几种设计模式有关([抽象]工厂,建筑商,请命名,我不喜欢研究其分类法)。2)这里讨论的实际问题是“如何将对象存储分配与对象构造完全脱钩?”。
科斯2012年

1
@丹尼斯:只有当你不这样做的delete时候。只要调用者获得了指针的所有权(请参阅:负责在适当时删除指针),这些方法就可以很好地工作(源代码是文档;-))。
鲍里斯·达尔斯泰因

1
@Boris @Dennis也可以通过返回a unique_ptr<T>而不是使其非常明确T*
科斯2014年

Answers:


107

首先,在某些情况下,对象构造是一个任务复杂到足以证明将其提取到另一个类的任务。

我认为这一点是不正确的。复杂性并不重要。相关性是做什么的。如果可以一步构建对象(与构建器模式不同),则构造器是正确的选择。如果您确实需要另一个类来执行此工作,则无论如何它应该是构造函数中使用的辅助类。

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

但好处是,您可以立即查看正在使用的坐标类型,同时不必担心复制。如果您要复制,而且价格昂贵(当然,通过性能分析证明),则可能希望使用Qt共享类之类的东西来避免复制开销。

对于分配类型,使用工厂模式的主要原因通常是多态。构造函数不能是虚拟的,即使可以,也没有太大意义。使用静态分配或堆栈分配时,您不能以多态方式创建对象,因为编译器需要知道确切的大小。因此,它仅适用于指针和引用。而且从工厂返回引用也不起作用,因为尽管从技术上讲,对象可以通过引用删除,但它可能会造成混乱且容易出错,请参阅返回C ++引用变量的做法是否有害?例如。因此,指针是唯一剩下的东西,它也包括智能指针。换句话说,工厂与动态分配一起使用时最有用,因此您可以执行以下操作:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

在其他情况下,工厂只会帮助解决一些小问题,例如您提到的超载问题。如果可以以统一的方式使用它们,那将是很好的选择,但是并没有太大的伤害,因为这可能是不可能的。


21
笛卡尔和Polar结构为+1。通常最好创建直接表示其预期数据的类和结构(与一般的Vec结构相反)。您的Factory也是一个很好的例子,但是您的例子没有说明谁拥有指针“ a”。如果工厂'f'拥有它,那么它可能会在'f'离开范围时被销毁,但是如果'f'不拥有它,对于开发人员来说,记住释放该内存很重要,否则内存泄漏可能发生。
David Peterson

1
当然可以通过引用删除对象!参见stackoverflow.com/a/752699/404734当然,这提出了一个问题,即通过引用返回动态内存是否明智,因为存在潜在地通过拷贝分配返回值的问题(调用者当然也可以做一些事情)就像int a = * returnsAPoninterToInt()一样,然后会遇到相同的问题,如果返回动态分配的内存,就像引用一样,但是在指针版本中,用户必须显式取消引用,而不仅仅是忘记显式引用,这是错误的。 。
Kaiserludi 2013年

1
@Kaiserludi,很高兴。我没有想到这一点,但这仍然是做事的“邪恶”方式。编辑我的答案以反映这一点。
谢尔盖·塔切诺夫

如何创建不可变的不同非多态类?那么,工厂模式是否适合在C ++中使用?
daaxix 2014年

@daaxix,为什么需要工厂来创建非多态类的实例?我不认为不变性与这一切有什么关系。
Sergei Tachenov 2014年

49

简单工厂示例:

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

2
@LokiAstari因为使用智能指针是释放对内存的控制的最简单方法。与其他语言相比,已知对C / C ++ lang的控制是至高无上的,并且从中获得最大的优势。更不用说智能指针会产生类似于其他托管语言的内存开销这一事实。如果您想要自动内存管理的便利,请开始使用Java或C#进行编程,但不要将这种混乱放入C / C ++中。
luke1985

45
unique_ptr该示例中的@ lukasz1985 没有性能开销。与其他任何语言相比,管理包括内存在内的资源都是C ++的最大优势之一,因为您可以在不损失性能的情况下确定性地做到这一点,而又不会失去控制权,但是您却恰恰相反。有些人不喜欢C ++隐式执行的操作,例如通过智能指针进行内存管理,但是如果要使所有内容都必须显式显示,请使用C;否则,请执行C。权衡问题要少几个数量级。我认为您不赞成一个好的建议是不公平的。
TheCppZoo 2015年

1
@EdMaster:我以前没有回应,因为他显然是在拖钓。请不要喂巨魔。
马丁·约克

17
@LokiAstari他可能是一个巨魔,但他说的话可能会使人们
感到

1
@yau:是的。但是:boost::ptr_vector<>效率更高一点,因为它知道它拥有指针而不是将工作委托给子类。但是其主要优点boost::ptr_vector<>是它通过引用(而不是指针)公开其成员,因此它很容易与标准库中的算法一起使用。
马丁·约克

41

您是否考虑过完全不使用工厂,而是更好地利用类型系统?我可以想到两种做这种事情的方法:

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

在允许您为每个构造函数拥有唯一的原型的同时,这也很好并且富有表现力。


29

您可以在以下位置阅读一个非常好的解决方案:http : //www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

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

根据这个想法,我已经做了工厂。请注意,我使用的是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");

17

我大多同意接受的答案,但是现有答案中没有涉及C ++ 11选项:

  • 按值返回工厂方法结果,并且
  • 提供便宜的move构造函数

例:

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

然后,您可以在堆栈上构造对象:

sandwich mine{sandwich::ham()};

作为其他事物的子对象:

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

或动态分配:

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

我什么时候可以使用?

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

我说“ 可能 ”是因为它取决于哪种方法可以给出最清晰的代码,而不会造成不必要的效率低下。


1
包装OpenGL资源时,我广泛使用了此方法。已删除的副本构造函数和副本分配会强制使用移动语义。然后,我创建了一堆静态工厂方法来创建每种类型的资源。这比OpenGL基于枚举的运行时分派更具可读性,后者通常根据传递的枚举具有一堆冗余函数参数。这是一个非常有用的模式,很奇怪这个答案没有更高。
鹅卵石

11

Loki同时具有工厂方法抽象工厂。两者都由Andei Alexandrescu 在Modern C ++ Design中进行了广泛的记录。尽管工厂方法仍然有些不同(虽然至少有内存可用,但它要求您在工厂可以创建该类型的对象之前注册一个类型),但工厂方法可能与您所追求的更接近。


1
即使它已经过时了(我对此表示怀疑),它仍然可以完美地使用。我仍然在新的C ++ 14项目中使用基于MC ++ D的Factory,效果很好!此外,Factory模式和Singleton模式可能是最不合时宜的部分。尽管Function可以用std::function和来代替Loki like 和type操作<type_traits>,而lambda,线程,右值引用可能需要一些细微调整,但他所描述的工厂并没有标准的替代品。
金属版

5

我不会回答所有问题,因为我认为它太广泛了。只是一些注意事项:

在某些情况下,对象构造是一项足够复杂的任务,足以证明将其提取为另一类。

该类实际上是Builder,而不是Factory。

在一般情况下,我不想强​​迫工厂的用户只能进行动态分配。

然后,您可以让工厂将其封装在智能指针中。我相信这样您就可以吃蛋糕了。

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

结论:通过返回对象建立工厂确实是某些情况下的解决方案(例如前面提到的2-D矢量),但仍不是构造函数的一般替代方法。

确实。所有设计模式都有其(特定于语言的)约束和缺点。建议仅在它们帮助您解决问题时才使用它们,而不是为了他们自己。

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


感谢您的回答!但是您能否解释一下使用智能指针将如何释放动态分配的限制?我不太明白这部分。
科斯,

@Kos,使用智能指针,您可以向用户隐藏实际对象的分配/取消分配。他们只看到封装的智能指针,它指向外界的行为就像一个静态分配的对象。
彼得Török

@Kos,不是严格意义上的AFAIR。您传入要包装的对象,该对象可能已在某个时刻动态分配。然后,智能指针将拥有它的所有权,并确保在不再需要智能指针时将其正确销毁(对于不同类型的智能指针,时间的决定将有所不同)。
彼得Török

3

这是我的c ++ 11样式解决方案。参数“ base”适用于所有子类的基类。创建者是用于创建子类实例的std :: function对象,可能是对子类的静态成员函数“ create(some args)”的绑定。这可能并不完美,但对我有用。这是一种“通用”解决方案。

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

对我来说很好 您将如何实现(也许是宏魔术)静态注册?试想一下,基类是对象的某种服务类。派生类为这些对象提供了一种特殊的服务。并且,您希望通过为每种服务添加从base派生的类来逐步添加各种服务。
St0fF

2

工厂模式

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

如果编译器不支持返回值优化,则放弃它,它可能根本不包含太多优化...


真的可以认为这是工厂模式的实现吗?
丹尼斯

1
@丹尼斯:作为一个退化的案例,我会这样认为。问题Factory在于它很通用并且涵盖了很多领域。例如,工厂可以添加参数(取决于环境/设置)或提供某些缓存(与Flyweight / Pool相关),但是这些情况仅在某些情况下才有意义。
Matthieu M.

如果仅更改编译器就象您听起来那样简单:)
rozina 2014年

@rozina::)在Linux上运行良好(gcc / clang非常兼容);我承认Windows仍然相对封闭,尽管它在64位平台上应该会更好(如果我没记错的话,专利的方式会更少)。
Matthieu M.

然后,您便拥有了一些低于标准的编译器的整个嵌入式世界。.::)我正在使用一种没有返回值优化的方法。我希望有。不幸的是,目前这不是一个选择。希望将来会进行更新,否则我们将进行其他切换:)
rozina 2014年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.