工厂设计模式如何处理不同长度的构造函数?
How to deal with constructors of differing length with the Factory design pattern?
我想要一个 class,它可以根据我传递给它的字符串创建不同种类的 objects。根据我的研究,这最能描述工厂设计模式。我成功地实现了它,但我 运行 遇到了一个设计问题:我不知道如何使用不同长度的构造函数创建 objects。
让我们以一个名为 Pet 的抽象 parent class 为例。其中有 3 children:鱼、猫和狗。它们都从 Pet 继承了重量和颜色,因此它们的构造函数中也有。但是一条鱼可能需要一些鳍和一个关于它是否是咸水鱼的布尔值。这是一个 4 参数构造函数。猫想要腿的数量。那是3个参数。这只狗可能有腿、品种、他是否与其他狗玩得好等参数,共 5 个参数。
在 C++ 中,我知道没有任何反射,所以最常见的做法似乎是只声明一个字符串到函数指针的映射,其中函数指针指向一个看起来像这样的函数:
template<typename T> Pet* createObject(int weight, std::string color) {return new T(weight, color);}
同样,我不确定如何在调用中填充更多参数而不影响其他 objects' 构造函数的调用。
我可以想到两个解决方法:让新函数接受不同数量的参数,或者为超过一定大小的构造函数设置默认参数。
根据我有多少不同的参数大小,解决方法 1 似乎有些过分。
解决方法 2 似乎忽略了构造函数的整个要点,因为我将在调用构造函数后被迫分配数据。
还有其他更好的解决方法吗?
这是你需要的吗(原谅内存泄漏之类的)
#include <map>
#include <string>
// definition of pet hierarcy
class pet_t
{
public:
virtual ~pet_t(void) {}
};
class frog_t : public pet_t
{
public:
frog_t(int) {}
static frog_t *builder(int n) { return new frog_t(n); }
};
class dog_t : public pet_t
{
public:
dog_t(const char *, int) {}
static dog_t *builder(const char *n, int p) { return new dog_t(n, p); }
};
// the per builder function type
typedef pet_t *(*pet_builder_t)(...);
// the map containing per builders: it's indexed by per type name
std::map<std::string, pet_builder_t> registry;
void build_factory(void)
{
registry["frog"] = reinterpret_cast<pet_builder_t>(&frog_t::builder);
registry["dog"] = reinterpret_cast<pet_builder_t>(&dog_t::builder);
}
// the actual factory function
template <class ...Ts>
pet_t *factory(std::string name, Ts&&...ts)
{
pet_builder_t builder = registry[name];
// assume there is something in the map
return builder(std::forward<Ts>(ts)...);
}
int main(int argc, char *argv[])
{
build_factory();
dog_t *dog = dynamic_cast<dog_t *>(factory(std::string("dog"), std::string("pluto"), 3));
frog_t *frog = dynamic_cast<frog_t *>(factory(std::string("frog"), 7));
}
我觉得演员表太多了,但这个想法应该是一个很好的起点。
您可以使用可变参数模板和完美转发。
template<typename T, typename... Args>
Pet* createObject(Args&&... args) {
return new T(std::forward<Args>(args)...);
}
但是,由于任何指针都可以转换为它的基数 class,如果这个函数 returns T*
可能会更好。此外,使用裸指针并不明智,因为您必须手动删除它们。最好使用 shared_ptr
or unique_ptr
. For these classes there already are similar factory methods: make_shared
and make_unique
(the latter only in C++14). Or, if your compiler doesn't support C++11, then you can use shared_ptr
and make_shared
from Boost.
当您在编译时知道需要创建什么类型时,当然可以使用此解决方案。如果你必须在运行时决定它,那么整个问题必须从不同的方向来考虑,就像你不知道你要创建什么类型,那么你就没有办法知道给他们什么参数,所有类型通用的参数除外。在这种情况下,您需要的是 abstract factory pattern。幸运的是,C++(至少从 C++11 开始)提供了一种无需创建大量 classes 即可实现此模式的方法。例如,假设您必须创建从 Pet
派生的某些 class 的实例。宠物的实际种类、大小和其他属性在其他地方决定,而宠物的名称在创建时决定。然后,你需要一个这样的工厂:
typedef std::function<std::shared_ptr<Pet>(const std::string& name)> PetFactory;
在某些时候,您决定要创建一个 Dog
(我将实际创建参数的含义留给您想象)。
PetFactory petFactory =
[](const std::string& name) {
return std::make_shared<Dog>(name, 12, "brown", 23.5);
}
实际创建时,只需调用工厂即可:
std::shared_ptr<Pet> pet = petFactory("Pet Name");
如果在创建一个对象时,您已经知道它的参数,并且知道它将是一个 Fish,那么您根本不需要工厂:只需构造一个 Fish 即可。
如果您不知道调用者会产生哪个对象,您可以合理地使用工厂。例如,您将字符串作为工厂方法的输入,也许是从文件中读取的:工厂通过解析字符串来创建和 returns 正确的对象类型。
调用者不知道它是鱼还是狗:这是工厂方法的目的。
此外,当您可以通过继承添加更多 "constructable" 对象并覆盖虚拟创造论者方法来扩展它时,您就可以使用工厂。如果方法具有不同的签名,则不会发生这种情况 - 它们实际上是不同的方法
这个问题有很多方法;我喜欢的可能不是最优雅的,但它非常明确。基本上,您创建一个到 boost::any
指针的映射,现在所有构造函数都简单地使用这样一个映射。
using MyArgs = unordered_map<string, boost::any>;
class Fish {
Fish(MyArgs args) {
int num_fins = boost::any_cast<int>(args.at("num_fins"));
}
现在你所有的构造函数都有相同的签名,所以你的工厂可以看起来像这样:
unique_ptr<Pet> factory(string animal_name, MyArgs args) {
auto func = factory_map.at(animal_name);
return func(args);
}
编辑:我还应该注意,如果您犯了一个错误,即您的 MyArgs 缺少一个参数或具有错误的类型之一,则会抛出异常。所以你可以得到一个很好的明确错误甚至处理它,而不是得到 UB。
很好玩!我求助于使用工厂子函数来解析参数本身的流。为了简化使用,我添加了注册器模式,当然还有大量的 TMP。
这里是删节代码:
/* Pet and derived classes omitted */
/* Registrar pattern omitted */
struct PetFactory {
using PetCtr = std::unique_ptr<Pet> (*)(std::istream &);
static auto make(std::istream &stream) {
std::string str;
stream >> str;
return ctrMap().at(str)(stream);
}
using PetCtrMap = std::map<std::string, PetCtr>;
static PetCtrMap &ctrMap();
};
template <class T, class... Args, std::size_t... Idx>
auto streamCtr_(std::istream &stream, std::index_sequence<Idx...> const &) {
std::tuple<Args...> args;
using unpack = int[];
unpack{0, (void(stream >> std::get<Idx>(args)), 0)...};
return std::make_unique<T>(std::move(std::get<Idx>(args))...);
}
template <class T, class... Args>
auto streamCtr(std::istream &stream) {
return std::unique_ptr<Pet>(streamCtr_<T, Args...>(
stream,
std::index_sequence_for<Args...>{}
));
}
int main() {
PetFactory::make("fish 1 silver 5 true");
PetFactory::make("cat 4 tabby 9");
PetFactory::make("dog 17 white husky playful");
}
输出:
I'm a 1kg silver fish with 5 fins, living in salty water.
I'm a 4kg tabby cat with 9 lives.
I'm a 17kg white husky and I'm playful.
完整的注释代码可用 here on Coliru。感谢挑战!
我想要一个 class,它可以根据我传递给它的字符串创建不同种类的 objects。根据我的研究,这最能描述工厂设计模式。我成功地实现了它,但我 运行 遇到了一个设计问题:我不知道如何使用不同长度的构造函数创建 objects。
让我们以一个名为 Pet 的抽象 parent class 为例。其中有 3 children:鱼、猫和狗。它们都从 Pet 继承了重量和颜色,因此它们的构造函数中也有。但是一条鱼可能需要一些鳍和一个关于它是否是咸水鱼的布尔值。这是一个 4 参数构造函数。猫想要腿的数量。那是3个参数。这只狗可能有腿、品种、他是否与其他狗玩得好等参数,共 5 个参数。
在 C++ 中,我知道没有任何反射,所以最常见的做法似乎是只声明一个字符串到函数指针的映射,其中函数指针指向一个看起来像这样的函数:
template<typename T> Pet* createObject(int weight, std::string color) {return new T(weight, color);}
同样,我不确定如何在调用中填充更多参数而不影响其他 objects' 构造函数的调用。
我可以想到两个解决方法:让新函数接受不同数量的参数,或者为超过一定大小的构造函数设置默认参数。
根据我有多少不同的参数大小,解决方法 1 似乎有些过分。
解决方法 2 似乎忽略了构造函数的整个要点,因为我将在调用构造函数后被迫分配数据。
还有其他更好的解决方法吗?
这是你需要的吗(原谅内存泄漏之类的)
#include <map>
#include <string>
// definition of pet hierarcy
class pet_t
{
public:
virtual ~pet_t(void) {}
};
class frog_t : public pet_t
{
public:
frog_t(int) {}
static frog_t *builder(int n) { return new frog_t(n); }
};
class dog_t : public pet_t
{
public:
dog_t(const char *, int) {}
static dog_t *builder(const char *n, int p) { return new dog_t(n, p); }
};
// the per builder function type
typedef pet_t *(*pet_builder_t)(...);
// the map containing per builders: it's indexed by per type name
std::map<std::string, pet_builder_t> registry;
void build_factory(void)
{
registry["frog"] = reinterpret_cast<pet_builder_t>(&frog_t::builder);
registry["dog"] = reinterpret_cast<pet_builder_t>(&dog_t::builder);
}
// the actual factory function
template <class ...Ts>
pet_t *factory(std::string name, Ts&&...ts)
{
pet_builder_t builder = registry[name];
// assume there is something in the map
return builder(std::forward<Ts>(ts)...);
}
int main(int argc, char *argv[])
{
build_factory();
dog_t *dog = dynamic_cast<dog_t *>(factory(std::string("dog"), std::string("pluto"), 3));
frog_t *frog = dynamic_cast<frog_t *>(factory(std::string("frog"), 7));
}
我觉得演员表太多了,但这个想法应该是一个很好的起点。
您可以使用可变参数模板和完美转发。
template<typename T, typename... Args>
Pet* createObject(Args&&... args) {
return new T(std::forward<Args>(args)...);
}
但是,由于任何指针都可以转换为它的基数 class,如果这个函数 returns T*
可能会更好。此外,使用裸指针并不明智,因为您必须手动删除它们。最好使用 shared_ptr
or unique_ptr
. For these classes there already are similar factory methods: make_shared
and make_unique
(the latter only in C++14). Or, if your compiler doesn't support C++11, then you can use shared_ptr
and make_shared
from Boost.
当您在编译时知道需要创建什么类型时,当然可以使用此解决方案。如果你必须在运行时决定它,那么整个问题必须从不同的方向来考虑,就像你不知道你要创建什么类型,那么你就没有办法知道给他们什么参数,所有类型通用的参数除外。在这种情况下,您需要的是 abstract factory pattern。幸运的是,C++(至少从 C++11 开始)提供了一种无需创建大量 classes 即可实现此模式的方法。例如,假设您必须创建从 Pet
派生的某些 class 的实例。宠物的实际种类、大小和其他属性在其他地方决定,而宠物的名称在创建时决定。然后,你需要一个这样的工厂:
typedef std::function<std::shared_ptr<Pet>(const std::string& name)> PetFactory;
在某些时候,您决定要创建一个 Dog
(我将实际创建参数的含义留给您想象)。
PetFactory petFactory =
[](const std::string& name) {
return std::make_shared<Dog>(name, 12, "brown", 23.5);
}
实际创建时,只需调用工厂即可:
std::shared_ptr<Pet> pet = petFactory("Pet Name");
如果在创建一个对象时,您已经知道它的参数,并且知道它将是一个 Fish,那么您根本不需要工厂:只需构造一个 Fish 即可。
如果您不知道调用者会产生哪个对象,您可以合理地使用工厂。例如,您将字符串作为工厂方法的输入,也许是从文件中读取的:工厂通过解析字符串来创建和 returns 正确的对象类型。
调用者不知道它是鱼还是狗:这是工厂方法的目的。
此外,当您可以通过继承添加更多 "constructable" 对象并覆盖虚拟创造论者方法来扩展它时,您就可以使用工厂。如果方法具有不同的签名,则不会发生这种情况 - 它们实际上是不同的方法
这个问题有很多方法;我喜欢的可能不是最优雅的,但它非常明确。基本上,您创建一个到 boost::any
指针的映射,现在所有构造函数都简单地使用这样一个映射。
using MyArgs = unordered_map<string, boost::any>;
class Fish {
Fish(MyArgs args) {
int num_fins = boost::any_cast<int>(args.at("num_fins"));
}
现在你所有的构造函数都有相同的签名,所以你的工厂可以看起来像这样:
unique_ptr<Pet> factory(string animal_name, MyArgs args) {
auto func = factory_map.at(animal_name);
return func(args);
}
编辑:我还应该注意,如果您犯了一个错误,即您的 MyArgs 缺少一个参数或具有错误的类型之一,则会抛出异常。所以你可以得到一个很好的明确错误甚至处理它,而不是得到 UB。
很好玩!我求助于使用工厂子函数来解析参数本身的流。为了简化使用,我添加了注册器模式,当然还有大量的 TMP。
这里是删节代码:
/* Pet and derived classes omitted */
/* Registrar pattern omitted */
struct PetFactory {
using PetCtr = std::unique_ptr<Pet> (*)(std::istream &);
static auto make(std::istream &stream) {
std::string str;
stream >> str;
return ctrMap().at(str)(stream);
}
using PetCtrMap = std::map<std::string, PetCtr>;
static PetCtrMap &ctrMap();
};
template <class T, class... Args, std::size_t... Idx>
auto streamCtr_(std::istream &stream, std::index_sequence<Idx...> const &) {
std::tuple<Args...> args;
using unpack = int[];
unpack{0, (void(stream >> std::get<Idx>(args)), 0)...};
return std::make_unique<T>(std::move(std::get<Idx>(args))...);
}
template <class T, class... Args>
auto streamCtr(std::istream &stream) {
return std::unique_ptr<Pet>(streamCtr_<T, Args...>(
stream,
std::index_sequence_for<Args...>{}
));
}
int main() {
PetFactory::make("fish 1 silver 5 true");
PetFactory::make("cat 4 tabby 9");
PetFactory::make("dog 17 white husky playful");
}
输出:
I'm a 1kg silver fish with 5 fins, living in salty water.
I'm a 4kg tabby cat with 9 lives.
I'm a 17kg white husky and I'm playful.
完整的注释代码可用 here on Coliru。感谢挑战!