如何正确实现C ++中的工厂方法模式
C ++中有这样一件事,使我长时间感到不舒服,因为我真的不知道该怎么做,尽管听起来很简单:
如何正确实现C ++中的Factory Method?
目标:使客户可以使用工厂方法而不是对象的构造函数实例化一些对象,而不会造成不可接受的后果和性能问题。
“工厂方法模式”是指对象内的静态工厂方法或其他类中定义的方法或全局函数。 一般来说,“将类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和一个位域)来重载,类型在语义上完全不同(理论上是有效的重载),但实际上却是同样的事情 - 像无符号整数或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);
}
}
在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中,对于上面的代码,不可复制不再是问题)
结论:通过返回对象来创建工厂确实是某些情况下的解决方案(例如之前提到的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
或引用成员变量, 而且可能还会有一些我现在无法想到的弊端,而且我从来没有觉得特别有必要,因为上面的要点已经说服了我。
所以:甚至没有接近实施工厂的一个好的通用解决方案。
结论:
我们希望有一种对象实例化的方式,它可以:
我相信我已经证明,我提到的方式不符合这些要求。
任何提示? 请给我提供一个解决方案,我不想认为这种语言不会让我正确实施这样一个小概念。
首先,有时候对象构建是一个复杂的任务,足以证明它可以提取到另一个类中。
我相信这一点是不正确的。 复杂性并不重要。 相关性是什么。 如果一个对象可以在一个步骤中构建(不像在构建器模式中),构造器是正确的地方。 如果你确实需要另一个类来执行这个工作,那么它应该是一个从构造函数中使用的帮助类。
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();
在其他情况下,工厂只是帮助解决你提到的超载问题。 如果有可能以统一的方式使用它们将会很好,但它可能不会造成太大的损害。
简单工厂示例:
// 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();
}
};
你有没有想过不使用工厂,而是很好地使用类型系统? 我可以想到两种不同的方法来做这种事情:
选项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);
这也是很好的和富有表现力的,同时允许你为每个构造函数设计独特的原型。
链接地址: http://www.djcxy.com/p/20363.html上一篇: How to implement the factory method pattern in C++ correctly