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

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

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


结论:

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

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

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

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


当前回答

我基本上同意已被接受的答案,但有一个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());

我什么时候可以用这个?

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

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

其他回答

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

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

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

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

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

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

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

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

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

你可以在: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");

我基本上同意已被接受的答案,但有一个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());

我什么时候可以用这个?

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

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

洛基有工厂方法和抽象工厂。两者都在Andei Alexandrescu的《现代c++设计》中有广泛的记录。factory方法可能更接近于您所追求的,尽管它仍然有一点不同(至少如果内存正常的话,它要求您在工厂创建该类型的对象之前注册一个类型)。

工厂模式

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

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