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

391

在C++中有一件事情让我感到不舒服已经很长时间了,因为我实际上不知道如何做到它,即使它听起来很简单:

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

目标:使客户端能够使用工厂方法来实例化某个对象,而不是使用对象的构造函数,同时避免不可接受的后果和性能损失。

我所说的“工厂方法模式”包括对象内的静态工厂方法、在另一个类中定义的方法或全局函数。总之,“将类X的正常实例化方式重定向到除构造函数以外的任何地方”的概念。

让我快速浏览一些我考虑过的可能答案。


0)不要制造工厂,制造构造函数。

这听起来不错(确实通常是最好的解决方案),但不是通用的解决办法。首先,有些情况下,对象构造是足够复杂的任务,需要将其提取到另一个类中进行。但即使将这个事实放在一边,对于只使用构造函数的简单对象,也经常行不通。

我知道的最简单的例子是一个二维向量类。非常简单,却很棘手。我希望能够同时从笛卡尔坐标和极坐标构造它。显然,我不能这样做:

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和位字段),这些类型在语义上完全不同(因此 - 在理论上 - 是有效的重载),但实际上却是相同的东西 - 就像无符号整数或void指针。


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++复杂的原因,但也经常使其强大。此外,我相信存在一些目标(关键字:嵌入式)不允许动态分配。这并不意味着这些平台的用户喜欢编写干净的面向对象程序。

无论如何,撇开哲学不谈:在一般情况下,我不想强制工厂的用户受到动态分配的限制。


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++标准足够仁慈,不会指定在返回按值临时对象时何时将对象原地创建和何时复制该对象。因此,如果复制Foo的成本很高,这种方法是有风险的。

如果Foo根本无法复制怎么办?那就糟糕了。(注意,在具有保证复制省略的C++17中,上述代码中的无法复制已经不再是问题)

结论:通过返回对象来创建工厂确实是一种解决某些情况(例如先前提到的2D向量)的方法,但仍不能普遍替代构造函数。


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,虽然标题非常相似,但是实际问题在我看来是不同的。 - Péter Török
4
好的,这个问题的重复已经存在,但是这个问题本身的文本也很有价值。 - dmckee --- ex-moderator kitten
9
两年之后重新思考这个问题,我有一些要补充的地方:1) 这个问题与多种设计模式([抽象]工厂、建造者等等)相关,我不喜欢深入探讨它们的分类。 2) 实际讨论的问题是“如何清晰地将对象存储分配与对象构造解耦?” - Kos
1
@Dennis:只有在您不delete它的情况下才可以这样做。只要“记录”(源代码是文档;-))调用者获取指针的所有权(即负责在适当时删除它),这种方法就完全没问题。 - Boris Dalstein
2
@Boris @Dennis,你们也可以通过返回unique_ptr<T>而不是T*来使其更加明确。 - Kos
显示剩余12条评论
11个回答

124

首先,有时对象构造是一项复杂的任务,足以证明其应该提取到另一个类中。

我认为这个观点是不正确的。复杂性并不重要,重要的是相关性。如果一个对象可以在一步内构建(不像建造者模式那样),那么构造函数就是合适的地方。如果您真正需要另一个类来执行此任务,那么它应该是一个辅助类,无论如何都会从构造函数中使用它。

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();
在其他情况下,工厂只是帮助解决像你提到的超载等小问题。如果可以以统一的方式使用它们,那将是很好的,但这可能是不可能的,并且这并没有太大的影响。

27
赞成使用笛卡尔和极坐标结构体。通常最好创建直接代表它们所需数据的类和结构体(而不是一般的Vec结构体)。您的工厂也是一个很好的例子,但您的示例没有说明谁拥有指针'a'。如果工厂'f'拥有它,那么当'f'离开作用域时,它可能会被销毁,但如果'f'不拥有它,开发人员记得释放该内存非常重要,否则可能会发生内存泄漏。 - David Peterson
1
当然,一个对象可以通过引用被删除!请参见https://dev59.com/1HRB5IYBdhLWcg3wAjJH#752699。这当然引出了一个问题,即是否明智地通过引用返回动态内存,因为可能会通过复制分配返回值的问题(调用者当然也可以像int a = *returnsAPoninterToInt()这样做,并且将面临相同的问题,如果返回动态分配的内存,就像引用一样,但在指针版本中,用户必须显式解除引用,而不是只是忘记显式引用,才会出错)。 - Kaiserludi
1
@Kaiserludi,说得好。我没有想到那一点,但这仍然是一种“邪恶”的做法。我编辑了我的答案以反映这一点。 - Sergei Tachenov
@daaxix,为什么你需要一个工厂来创建非多态类的实例?我不明白这与不可变性有什么关系。 - Sergei Tachenov
1
@Kevin:相反地,我主张在所有地方都使用“极坐标”和“直角坐标”,而不是“Vec2”。基本上,除了数学运算(通过为您的类型重载运算符)外,Vec2 没有太多接口,还可以通过定义转换运算符进行相互转换,以及通过定义单个重载的自由函数来规范化。重载是 C++ 的一部分,并作为类型的大型不可见编译时 if-else 进行操作。运算符重载和隐式/显式转换也是 C++ 的一部分,它们是添加到 C++ 中的特性。我们不应该与这门语言斗争。 - Laurent LA RIZZA
显示剩余10条评论

54

简单工厂示例:

// 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++ 语言在内存控制方面被认为是最优秀的,并从中获得了最大的优势。更不用说智能指针会产生类似于其他托管语言的内存开销。如果你想要自动内存管理的便利性,请开始学习 Java 或 C# 编程,但不要把这种混乱引入到 C/C++ 中。 - luke1985
51
@lukasz1985 这个例子中的 unique_ptr 没有性能开销。管理资源,包括内存,是 C++ 相对于其他语言的最大优势之一,因为你可以在没有性能损失并且有确定性的情况下进行资源管理,而不会失去控制。但你却恰恰说相反的话。有些人不喜欢 C++ 隐式地执行内存管理,比如通过智能指针,但如果你想让所有东西都强制变成显式的,那就使用 C;这种情况下,付出的代价将是数倍于问题数量的。我认为你对一个好的建议投反对票是不公平的。 - TheCppZoo
1
@EdMaster:我之前没有回应是因为他显然在恶意挑衅。请不要理会这个喷子。 - Martin York
19
他可能是个喷子,但他说的话可能会让人感到困惑。 - TheCppZoo
1
@yau:是的。但是:boost::ptr_vector<>略微更高效,因为它理解它拥有指针而不是将工作委托给子类。但是 boost::ptr_vector<> 的主要优点是通过引用(而不是指针)公开其成员,因此可以很容易地与标准库中的算法一起使用。 - Martin York
显示剩余5条评论

45

你有没有考虑过不使用工厂,而是更好地利用类型系统?我能想到两种不同的方法来完成这件事:

选项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:

您可以使用“标记(tags)”,就像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

最好的解决方案在“评论和讨论”中,看到“不需要静态创建方法”。

基于这个想法,我做了一个工厂。请注意,我正在使用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");

在我看来,这个概念违背了工厂方法模式的初衷,即隐藏生成对象的实现。这需要调用者了解派生类和基类,从而削弱了其实用性。如果我想要创建一个实现接口的对象,而我还必须知道构建的具体细节,那么我就不需要模板帮我完成了。工厂方法模式描述了具体和抽象工厂接口,其中每个具体工厂可以构建特定类型的对象。 - shawn1874
这个想法不错,但如果派生类的构造函数需要参数,则无法工作。 - BЈовић

21

我大体上同意被接受的答案,但现有的回答并未涵盖一个C++11选项:

  • 通过值返回工厂方法的结果,以及
  • 提供便宜的移动构造函数。

例如:

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

何时会使用它?

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

我说 '可能' 是因为这取决于哪种方法能够提供清晰的代码而不会不必要地降低效率。


2
我在封装OpenGL资源时广泛使用了这种方法。删除了复制构造函数和复制赋值,强制使用移动语义。然后我创建了一堆静态工厂方法来创建每种类型的资源。这比OpenGL的基于枚举的运行时分派更可读,而后者通常具有取决于传递的枚举的重复函数参数。这是一种非常有用的模式,惊讶的是这个答案排名不高。 - Fibbs

12

Loki有一个工厂方法和一个抽象工厂。这两者都在Andei Alexandrescu的《现代C++设计》中有详细文档说明。工厂方法可能更接近于你想要的,但它仍然有所不同(如果我没记错的话,它要求在工厂创建该类型的对象之前先注册该类型)。


1
即使它已经过时(我对此持有异议),它仍然完全可用。我在一个新的C++14项目中仍然使用基于MC++D的工厂,效果非常好!此外,工厂和单例模式可能是最不过时的部分。虽然像“Function”和类型操作这样的Loki片段可以用“std::function”和“<type_traits>”替换,而lambda、线程、右值引用具有可能需要进行一些微小调整的影响,但没有标准替代品可以取代他所描述的单例或工厂。 - metal

5
我不尝试回答所有问题,因为我认为这太笼统了。只有几点需要注意:

有些情况下,对象构建是足够复杂的任务,可以将其提取到另一个类中。

那个类实际上是一个Builder,而不是工厂。

一般情况下,我不想强制工厂的用户受到动态分配的限制。

然后你可以让你的工厂用智能指针来封装它。我相信这样你既可以拥有你的蛋糕,也可以吃掉它。

这也消除了与按值返回相关的问题。

结论:通过返回对象来创建工厂确实是某些情况下(例如先前提到的二维向量)的解决方案,但仍不能普遍替代构造函数。

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

如果你追求“完美”的工厂实现,好运。


谢谢你的回答!但是你能解释一下如何使用智能指针来释放动态分配的限制吗?我没有完全理解这部分。 - Kos
@Kos,使用智能指针可以将实际对象的分配/释放隐藏在用户之外。他们只看到封装的智能指针,对外表现为静态分配的对象。 - Péter Török
@Kos,不是严格意义上的,AFAIR。您将要包装的对象传递进去,这个对象可能在某个时刻动态分配了内存。然后智能指针接管它,并确保在不再需要它时(对于不同类型的智能指针,决定时间不同)正确地销毁它。 - Péter Török

3

这是我的C++11风格的解决方案。参数“base”是所有子类的基类。创建者是std::function对象,用于创建子类实例,可能绑定到您的子类的静态成员函数“create(某些参数)”。这可能不完美,但对我有效。它是一种“通用”的解决方案。

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

看起来不错。你会如何实现(也许需要一些宏魔法)静态注册?想象一下基类是一些对象的服务类。派生类为这些对象提供特殊的服务。你想通过添加从基类派生的类来逐步添加不同类型的服务。每种服务都对应一个派生类。 - St0fF

2

工厂模式

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

如果你的编译器不支持返回值优化,就试着换一个吧,它可能根本没有进行太多优化...

1
@Dennis:作为一种退化情况,我认为是这样的。Factory 的问题在于它非常通用,涵盖了很多领域;例如,工厂可以添加参数(取决于环境/设置),或者提供一些缓存(与 Flyweight/Pools 相关),但这些情况只在某些情况下才有意义。 - Matthieu M.
如果只有更改编译器像你说的那样容易就好了 :) - rozina
@rozina: :) 在Linux上运行良好(gcc/clang非常兼容);我承认Windows仍然相对封闭,但在64位平台上应该会变得更好(如果我没记错的话,专利问题会少一些)。 - Matthieu M.
然后你会发现整个嵌入式世界都有一些次级的编译器.. :) 我正在使用这样一个编译器,它没有返回值优化。虽然我希望它有这个功能。但不幸的是,目前无法更换。希望未来会更新或者我们会转向其他的东西 :) - rozina
我向我们编译器的开发人员询问了这个问题,他们正在做这件事,哈哈。不过预计需要2年时间 :) - rozina
显示剩余2条评论

2
extern std::pair<std::string_view, Base*(*)()> const factories[2];

decltype(factories) factories{
  {"blah", []() -> Base*{return new Blah;}},
  {"foo", []() -> Base*{return new Foo;}}
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接