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

Kos*_*Kos 309 c++ design-patterns idioms factory-method

C++中的这一件事让我感到不舒服很长一段时间,因为我老实说不知道该怎么做,尽管听起来很简单:

如何正确地在C++中实现Factory方法?

目标:允许客户端使用工厂方法而不是对象的构造函数来实例化某个对象,而不会产生不可接受的后果和性能损失.

"工厂方法模式"是指对象内部的静态工厂方法或另一个类中定义的方法,或全局函数.通常只是"将类X的实例化的正常方式重定向到构造函数之外的任何其他位置的概念".

让我略过一些我想到过的可能答案.


0)不要制造工厂,制造建造者.

这听起来不错(实际上通常是最好的解决方案),但不是一般的补救措施.首先,有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类.但即使将这个事实放在一边,即使对于仅使用构造函数的简单对象,通常也不会这样做.

我所知道的最简单的例子是2-D Vector类.这么简单,但很棘手.我希望能够从笛卡尔坐标和极坐标两者构造它.显然,我做不到:

struct Vec2 {
    Vec2(float x, float y);
    Vec2(float angle, float magnitude); // not a valid overload!
    // ...
};
Run Code Online (Sandbox Code Playgroud)

我的自然思维方式是:

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    // ...
};
Run Code Online (Sandbox Code Playgroud)

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

另一种情况:试图通过某些API的两个opaque typedef(例如不相关域的GUID,或GUID和位域)重载,类型在语义上完全不同(所以 - 理论上 - 有效的重载)但实际上它们实际上是同样的事情 - 像无符号的int或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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在C++中,这转换为:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};
Run Code Online (Sandbox Code Playgroud)

凉?确实经常.但随后 - 这迫使用户仅使用动态分配.静态分配是使C++变得复杂的原因,也是使它变得强大的原因.另外,我认为存在一些不允许动态分配的目标(关键字:嵌入式).这并不意味着这些平台的用户喜欢编写干净的OOP.

无论如何,哲学不谈:在一般情况下,我不想强​​迫工厂的用户限制动态分配.


2)按价值返回

好的,所以我们知道1)在我们想要动态分配时很酷.为什么我们不在其上添加静态分配?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};
Run Code Online (Sandbox Code Playgroud)

什么?我们不能通过返回类型重载?哦,当然我们不能.所以让我们改变方法名称来反映这一点.是的,我上面写的无效代码示例只是为了强调我不喜欢需要更改方法名称,例如因为我们现在无法正确实现与语言无关的工厂设计,因为我们必须更改名称 - 和此代码的每个用户都需要记住实现与规范的区别.

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};
Run Code Online (Sandbox Code Playgroud)

好的......我们有它.这很难看,因为我们需要更改方法名称.这是不完美的,因为我们需要两次编写相同的代码.但一旦完成,它就有效.对?

嗯,通常.但有时却没有.在创建Foo时,我们实际上依赖于编译器来为我们做返回值优化,因为C++标准对于编译器供应商而言是足够的,不会指定对象何时就地创建以及何时在返回时复制它C++中按值的临时对象.因此,如果Foo复制起来很昂贵,这种方法是有风险的.

如果Foo根本不可复制怎么办?好吧,doh.(请注意,在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());
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

既然我写了所有这些并将其作为最后一个,我也必须不喜欢它.:)为什么?

首先......我真诚地不喜欢两阶段结构的概念,当我使用它时我感到内疚.如果我设置我的对象的断言"如果它存在,它处于有效状态",我觉得我的代码更安全,更不容易出错.我喜欢这样.

不得不放弃那个约定并改变我的对象的设计只是为了制造它的工厂是..好吧,笨拙.

我知道上述内容不会说服很多人,所以让我给出一些更为坚实的论据.使用两阶段构造,您不能:

  • 初始化const或引用成员变量,
  • 将参数传递给基类构造函数和成员对象构造函数.

可能还有一些我现在无法想到的缺点,我甚至不觉得特别有责任,因为上面的要点已经说服了我.

所以:甚至没有接近实施工厂的良好通用解决方案.


结论:

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

  • 允许统一实例化,无论分配如何,
  • 给构造方法赋予不同的,有意义的名称(因此不依赖于副参数重载),
  • 没有引入显着的性能损失,并且最好是显着的代码膨胀,特别是在客户端,
  • 是一般的,如:可能被引入任何类.

我相信我已经证明我提到的方式不符合这些要求.

任何提示?请给我一个解决方案,我不想认为这种语言不会让我正确地实现这样一个琐碎的概念.

Ser*_*nov 98

首先,有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类.

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

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
Run Code Online (Sandbox Code Playgroud)

有一个简单的解决方法:

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);
Run Code Online (Sandbox Code Playgroud)

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

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));
Run Code Online (Sandbox Code Playgroud)

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

至于分配类型,使用工厂模式的主要原因通常是多态.构造函数不能是虚拟的,即使它们可以,也没有多大意义.使用静态或堆栈分配时,无法以多态方式创建对象,因为编译器需要知道确切的大小.所以它只适用于指针和引用.并且从工厂返回引用也不起作用,因为虽然技术上可以通过引用删除对象,但它可能相当混乱且容易出错,请参阅返回C++引用变量的做法,邪恶?例如.所以指针是唯一剩下的东西,包括智能指针.换句话说,工厂在与动态分配一起使用时最有用,所以你可以这样做:

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

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

Factory f;
Abstract *a = f.create();
a->do();
Run Code Online (Sandbox Code Playgroud)

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

  • 笛卡儿和极地结构的+1.通常最好创建直接表示它们所针对的数据的类和结构(而不​​是一般的Vec结构).你的工厂也是一个很好的例子,但你的例子没有说明谁拥有指针'a'.如果工厂'f'拥有它,那么它可能会在'f'离开范围时被销毁,但如果'f'不拥有它,那么开发人员要记住释放内存或者内存泄漏是很重要的发生. (19认同)

Mar*_*ork 45

简单工厂示例:

// 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();
    }
};
Run Code Online (Sandbox Code Playgroud)

  • @ lukasz1985该示例中的`unique_ptr`没有性能开销.管理资源(包括内存)是C++相对于任何其他语言的最大优势之一,因为你可以在没有性能损失的情况下确定性地执行它,而不会失去控制,但你说的恰恰相反.有些人不喜欢C++隐含的东西,比如通过智能指针进行内存管理,但如果你想要的是一切都是强制性的,那就用C; 权衡要少几个数量级的问题.我认为你推荐一个好的建议是不公平的. (39认同)
  • @LokiAstari他可能是一个巨魔,但他说的可能会让人感到困惑 (16认同)
  • @ lukasz1985:为什么? (3认同)
  • @LokiAstari 因为使用智能指针是失去对内存控制的最简单方法。与其他语言相比,控制哪些 C/C++ langs 是至高无上的,并且它们从中获得了最大的优势。更不用说智能指针产生类似于其他托管语言的内存开销这一事实。如果您想要自动内存管理的便利,请开始使用 Java 或 C# 进行编程,但不要将这些混乱放入 C/C++ 中。 (2认同)

Eva*_*ran 40

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

选项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) { /* ... */ }
};
Run Code Online (Sandbox Code Playgroud)

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

Vec2 v(linear(1.0, 2.0));
Run Code Online (Sandbox Code Playgroud)

选项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 &) { /* ... */ }
};
Run Code Online (Sandbox Code Playgroud)

第二种方法允许您编写如下代码:

Vec2 v(1.0, 2.0, linear_coord);
Run Code Online (Sandbox Code Playgroud)

这也是很好的和富有表现力的,同时允许你为每个构造函数提供独特的原型.


mab*_*abg 27

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

最好的解决方案是"评论和讨论",请参阅"不需要静态创建方法".

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

#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
Run Code Online (Sandbox Code Playgroud)

样品用法:

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");
Run Code Online (Sandbox Code Playgroud)


mbr*_*knl 16

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

  • 按值返回工厂方法结果,和
  • 提供廉价的移动构造函数.

例:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};
Run Code Online (Sandbox Code Playgroud)

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

sandwich mine{sandwich::ham()};
Run Code Online (Sandbox Code Playgroud)

作为其他事物的子对象:

auto lunch = std::make_pair(sandwich::spam(), apple{});
Run Code Online (Sandbox Code Playgroud)

或动态分配:

auto ptr = std::make_shared<sandwich>(sandwich::ham());
Run Code Online (Sandbox Code Playgroud)

我什么时候可以用这个?

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

我说' 可能 ',因为它取决于哪种方法提供最清晰的代码而不会产生不必要的低效率.

  • 我在包装 OpenGL 资源时广泛使用了它。删除了复制构造函数和复制赋值,强制使用移动语义。然后,我创建了一堆静态工厂方法来创建每种类型的资源。这比 OpenGL 基于枚举的运行时调度更具可读性,后者通常具有一堆冗余函数参数,具体取决于传递的枚举。这是一个非常有用的模式,令人惊讶的是这个答案并不高。 (2认同)

Jer*_*fin 11

Loki有工厂方法抽象工厂.两者都在Andei Alexandrescu的Modern C++ Design中进行了详细记录.工厂方法可能更接近你所看到的,虽然它仍然有点不同(至少如果内存服务,它需要你在工厂创建该类型的对象之前注册一个类型).

  • 该代码在C ++ 11中已经过时了。 (2认同)

Pét*_*rök 5

我不会试图回答我的所有问题,因为我认为它太宽泛了.只是几个笔记:

有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类.

那个类实际上是一个Builder,而不是Factory.

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

然后你可以让你的工厂将它封装在一个智能指针中.我相信这样你就可以吃蛋糕了.

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

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

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

如果你是在"完美"的工厂实施后,那么,祝你好运.