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

新手上路,请多包涵

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 和位域),语义上完全不同的类型(因此 - 在理论上 - 有效重载)但实际上结果是同样的事情——比如无符号整数或空指针。


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) 按值返回

好的,所以我们知道 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 中,对于上面的代码,不可复制不再是问题_)

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


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

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

所以:甚至没有一个很好的通用解决方案来实现工厂。


结论:

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

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

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

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

原文由 Kos 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 373
2 个回答

首先,在某些情况下,对象构造是一项复杂到足以证明将其提取到另一个类的任务。

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

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

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

原文由 Sergei Tachenov 发布,翻译遵循 CC BY-SA 3.0 许可协议

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

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

原文由 user1095108 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题