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

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

There's this one thing in C++ which has been making me feel uncomfortable for quite a long time, because I honestly don't know how to do it, even though it sounds simple:

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

Goal: to make it possible to allow the client to instantiate some object using factory methods instead of the object's constructors, without unacceptable consequences and a performance hit.

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

By "Factory method pattern", I mean both static factory methods inside an object or methods defined in another class, or global functions. Just generally "the concept of redirecting the normal way of instantiation of class X to anywhere else than the constructor".

让我浏览一下我想到的一些可能的答案.

Let me skim through some possible answers which I have thought of.

这听起来不错(实际上通常是最好的解决方案),但不是一般的补救措施.首先,在某些情况下,对象构造是一项足够复杂的任务,足以证明其提取到另一个类是合理的.但即使把这个事实放在一边,即使对于只使用构造函数的简单对象,通常也行不通.

This sounds nice (and indeed often the best solution), but is not a general remedy. First of all, there are cases when object construction is a task complex enough to justify its extraction to another class. But even putting that fact aside, even for simple objects using just constructors often won't do.

我知道的最简单的例子是二维向量类.如此简单,但也很棘手.我希望能够从笛卡尔坐标和极坐标中构建它.显然,我不能这样做:

The simplest example I know is a 2-D Vector class. So simple, yet tricky. I want to be able to construct it both from both Cartesian and polar coordinates. Obviously, I cannot do:

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 点中对此进行描述.请继续阅读.

Which, instead of constructors, leads me to usage of static factory methods... which essentially means that I'm implementing the factory pattern, in some way ("the class becomes its own factory"). This looks nice (and would suit this particular case), but fails in some cases, which I'm going to describe in point 2. Do read on.

另一种情况:试图通过某些 API 的两个不透明 typedef(例如不相关域的 GUID,或 GUID 和位域)进行重载,类型在语义上完全不同(因此 - 理论上 - 有效的重载)但是实际上结果是一样的东西――比如无符号整数或空指针.

Java 很简单,因为我们只有动态分配的对象.建立工厂就像:

Java has it simple, as we only have dynamic-allocated objects. Making a factory is as trivial as:

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++ 中,这转化为:

In C++, this translates to:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

酷吗?很多时候,确实如此.但是,这迫使用户只能使用动态分配.静态分配使 C++ 变得复杂,但也常常使其强大.另外,我相信存在一些不允许动态分配的目标(关键字:嵌入式).这并不意味着这些平台的用户喜欢编写干净的 OOP.

Cool? Often, indeed. But then- this forces the user to only use dynamic allocation. Static allocation is what makes C++ complex, but is also what often makes it powerful. Also, I believe that there exist some targets (keyword: embedded) which don't allow for dynamic allocation. And that doesn't imply that the users of those platforms like to write clean OOP.

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

Anyway, philosophy aside: In the general case, I don't want to force the users of the factory to be restrained to dynamic allocation.

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

OK, so we know that 1) is cool when we want dynamic allocation. Why won't we add static allocation on top of that?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};

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

What? We can't overload by the return type? Oh, of course we can't. So let's change the method names to reflect that. And yes, I've written the invalid code example above just to stress how much I dislike the need to change the method name, for example because we cannot implement a language-agnostic factory design properly now, since we have to change names - and every user of this code will need to remember that difference of the implementation from the specification.

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};

好的……我们有了.这很丑陋,因为我们需要更改方法名称.这是不完美的,因为我们需要编写两次相同的代码.但是一旦完成,它就起作用了.对吗?

OK... there we have it. It's ugly, as we need to change the method name. It's imperfect, since we need to write the same code twice. But once done, it works. Right?

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

Well, usually. But sometimes it does not. When creating Foo, we actually depend on the compiler to do the return value optimisation for us, because the C++ standard is benevolent enough for the compiler vendors not to specify when will the object created in-place and when will it be copied when returning a temporary object by value in C++. So if Foo is expensive to copy, this approach is risky.

如果 Foo 根本不可复制怎么办?嗯,嗯.(请注意,在保证复制省略的 C++17 中,对于上面的代码来说,不可复制不再是问题)

And what if Foo is not copiable at all? Well, doh. (Note that in C++17 with guaranteed copy elision, not-being-copiable is no problem anymore for the code above)

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

Conclusion: Making a factory by returning an object is indeed a solution for some cases (such as the 2-D vector previously mentioned), but still not a general replacement for constructors.

另一个人可能会想到的事情是将对象分配和初始化的问题分开.这通常会导致如下代码:

Another thing that someone would probably come up with is separating the issue of object allocation and its initialisation. This usually results in code like this:

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

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

One may think it works like a charm. The only price we pay for in our code...

既然我已经写了所有这些并且把它留到最后,我也一定不喜欢它.:) 为什么?

Since I've written all of this and left this as the last, I must dislike it too. :) Why?

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

First of all... I sincerely dislike the concept of two-phase construction and I feel guilty when I use it. If I design my objects with the assertion that "if it exists, it is in valid state", I feel that my code is safer and less error-prone. I like it that way.

不得不放弃那个惯例并仅仅为了制造它的工厂而改变我的对象的设计......好吧,笨拙.

Having to drop that convention AND changing the design of my object just for the purpose of making factory of it is.. well, unwieldy.

我知道以上内容不能说服很多人,所以让我提供一些更可靠的论据.使用两阶段构造,您不能:

I know that the above won't convince many people, so let's me give some more solid arguments. Using two-phase construction, you cannot:

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

而且可能还有一些我现在想不到的缺点,而且我什至没有感到特别有义务,因为上述要点已经说服了我.

And probably there could be some more drawbacks which I can't think of right now, and I don't even feel particularly obliged to since the above bullet points convince me already.

所以:甚至不是实现工厂的好的通用解决方案.

So: not even close to a good general solution for implementing a factory.

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

We want to have a way of object instantiation which would:

  • 无论分配如何,都允许统一实例化,
  • 为构造方法指定不同的、有意义的名称(因此不依赖于参数重载),
  • 不会导致显着的性能损失,最好不会导致显着的代码膨胀,尤其是在客户端,
  • 一般,例如:可以为任何类引入.

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

I believe I have proven that the ways I have mentioned don't fulfil those requirements.

有什么提示吗?请给我一个解决方案,我不想认为这种语言不允许我正确实现这样一个微不足道的概念.

Any hints? Please provide me with a solution, I don't want to think that this language won't allow me to properly implement such a trivial concept.

推荐答案

首先,有这样的情况对象构建是一项复杂的任务足以证明它的提取是合理的另一堂课.

First of all, there are cases when object construction is a task complex enough to justify its extraction to another class.

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

I believe this point is incorrect. The complexity doesn't really matter. The relevance is what does. If an object can be constructed in one step (not like in the builder pattern), the constructor is the right place to do it. If you really need another class to perform the job, then it should be a helper class that is used from the constructor anyway.

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

对此有一个简单的解决方法:

There is an easy workaround for this:

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

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

The only disadvantage is that it looks a bit verbose:

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

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

But the good thing is that you can immediately see what coordinate type you're using, and at the same time you don't have to worry about copying. If you want copying, and it's expensive (as proven by profiling, of course), you may wish to use something like Qt's shared classes to avoid copying overhead.

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

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

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

In other cases, factories just help to solve minor problems like those with overloads you have mentioned. It would be nice if it was possible to use them in a uniform way, but it doesn't hurt much that it is probably impossible.

相关文章