工厂设计模式如何处理不同长度的构造函数?

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。感谢挑战!