Kos*_*Kos 309 c++ design-patterns idioms factory-method
C++中的这一件事让我感到不舒服很长一段时间,因为我老实说不知道该怎么做,尽管听起来很简单:
目标:允许客户端使用工厂方法而不是对象的构造函数来实例化某个对象,而不会产生不可接受的后果和性能损失.
"工厂方法模式"是指对象内部的静态工厂方法或另一个类中定义的方法,或全局函数.通常只是"将类X的实例化的正常方式重定向到构造函数之外的任何其他位置的概念".
让我略过一些我想到过的可能答案.
这听起来不错(实际上通常是最好的解决方案),但不是一般的补救措施.首先,有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类.但即使将这个事实放在一边,即使对于仅使用构造函数的简单对象,通常也不会这样做.
我所知道的最简单的例子是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指针.
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.
无论如何,哲学不谈:在一般情况下,我不想强迫工厂的用户限制动态分配.
好的,所以我们知道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向量),但仍然不是构造函数的一般替代.
有人可能想出的另一件事是分离对象分配和初始化的问题.这通常导致代码如下:
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)
在其他情况下,工厂只是帮助解决您提到的超载问题等小问题.如果有可能以统一的方式使用它们会很好,但它可能不会造成太大的伤害.
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)
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)
我什么时候可以用这个?
如果在公共构造函数中,如果没有一些初步计算,就不可能为所有类成员提供有意义的初始化器,那么我可能会将该构造函数转换为静态方法.静态方法执行初步计算,然后通过私有构造函数返回值结果,该构造函数仅执行成员初始化.
我说' 可能 ',因为它取决于哪种方法提供最清晰的代码而不会产生不必要的低效率.
我不会试图回答我的所有问题,因为我认为它太宽泛了.只是几个笔记:
有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类.
那个类实际上是一个Builder,而不是Factory.
在一般情况下,我不想强迫工厂的用户限制动态分配.
然后你可以让你的工厂将它封装在一个智能指针中.我相信这样你就可以吃蛋糕了.
这也消除了与按价值返回相关的问题.
结论:通过返回对象来建立工厂确实是某些情况的解决方案(例如前面提到的2-D向量),但仍然不是构造函数的一般替代.
确实.所有设计模式都有其(语言特定的)约束和缺点.建议仅在它们帮助您解决问题时使用它们,而不是为了它们自己.
如果你是在"完美"的工厂实施后,那么,祝你好运.