c++中有一件事一直让我感到不舒服,因为我真的不知道该怎么做,尽管它听起来很简单:

我如何在c++中正确地实现工厂方法?

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

通过“工厂方法模式”,我指的是对象中的静态工厂方法或定义在另一个类中的方法,或全局函数。只是一般的“将类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的两个不透明的类型定义重载(比如不相关域的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) Return-by-value

好的,我们知道当我们需要动态分配时,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或引用成员变量, 将参数传递给基类构造函数和成员对象构造函数。

可能还会有一些我现在想不到的缺点,我甚至不觉得有什么特别的义务,因为上面的要点已经说服了我。

所以:对于实现工厂来说,甚至还没有一个好的通用解决方案。


结论:

我们希望有一种对象实例化的方式:

无论分配如何,都允许统一的实例化, 给构造方法取不同的、有意义的名字(这样就不依赖于参数重载), 不要带来严重的性能损失,最好是严重的代码膨胀,尤其是在客户端, 是一般的,例如:可以为任何类引入。

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

有提示吗?请给我一个解决方案,我不想认为这个语言不能让我正确地实现这样一个微不足道的概念。


当前回答

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

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

其他回答

工厂模式

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

如果你的编译器不支持返回值优化,抛弃它,它可能根本不包含很多优化…

首先,有些情况下 对象构造是一项复杂的任务 足以证明它的开采 另一个类。

我认为这一点是不正确的。复杂性并不重要。相关性是起作用的东西。如果一个对象可以在一个步骤中构造(不像在构造器模式中那样),构造函数就是正确的地方。如果确实需要另一个类来执行任务,那么它应该是一个从构造函数中使用的helper类。

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的共享类这样的东西来避免复制开销。

As for the allocation type, the main reason to use the factory pattern is usually polymorphism. Constructors can't be virtual, and even if they could, it wouldn't make much sense. When using static or stack allocation, you can't create objects in a polymorphic way because the compiler needs to know the exact size. So it works only with pointers and references. And returning a reference from a factory doesn't work too, because while an object technically can be deleted by reference, it could be rather confusing and bug-prone, see Is the practice of returning a C++ reference variable, evil? for example. So pointers are the only thing that's left, and that includes smart pointers too. In other words, factories are most useful when used with dynamic allocation, so you can do things like this:

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

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

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

在其他情况下,工厂只是帮助解决一些小问题,比如你提到的过载问题。如果能够以统一的方式使用它们,那就太好了,但这可能是不可能的,这也没什么坏处。

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

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

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

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

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

我不想回答我所有的问题,因为我认为这太宽泛了。只是一些注释:

在某些情况下,对象构造是一项非常复杂的任务,需要将其提取到另一个类。

这个类实际上是一个生成器,而不是一个工厂。

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

然后你可以让你的工厂把它封装在一个智能指针中。我相信这样你可以鱼与熊掌兼得。

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

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

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

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