在C++中实现可扩展工厂的优雅方式

Mar*_*sen 24 c++

我正在寻找一种直观和可扩展的方法来实现给定基类的子类的工厂.我想在库中提供这样的工厂函数.棘手的部分是我希望工厂也为用户定义的子类工作(例如,让库的工厂函数构建不同的子类,具体取决于链接到它的模块).目标是为下游开发人员使用工厂带来最小的负担/困惑.

我想要做的一个例子是:给定a std::istream,构造并返回与内容匹配的子类的对象,如果没有找到匹配则返回空指针.全球工厂的签名如下:

Base* Factory(std::istream &is){ ... };
Run Code Online (Sandbox Code Playgroud)

我熟悉原型工厂,但我更愿意避免制作/存储原型对象.这里发布了一个相关的问题::使用工厂允许最大的灵活性/可扩展性.

我目前不是在寻找特定的解决方案,但如果它们更优雅,我会很乐意了解这些.

我提出了一个工作解决方案,我相信它相当优雅,我将作为答案发布.我可以想象这个问题相当普遍,所以我想知道是否有人知道更好的方法.

编辑:似乎有些澄清是为了......

这个想法是让工厂构造派生类的对象,而不包含决定哪一个的逻辑.更糟糕的是,工厂方法最终将作为库的一部分,派生类可以在插件中定义.

派生类必须能够根据提供的输入(例如输入文件)自行决定它们是否适合构造.正如几个人所建议的那样,这个决定可以作为工厂可以使用的谓词来实现(顺便提一下,很好的建议!).

Sue*_*ode 9

如果我理解正确,我们需要一个工厂函数,可以根据构造函数输入选择要实例化的派生类.这是迄今为止我能提出的最通用的解决方案.您可以指定映射输入以组织工厂函数,然后可以在工厂调用时指定构造函数输入.我不愿意说代码解释的比我说的要多,但我认为FactoryGen.hin 的示例实现Base.h并且Derived.h在注释的帮助下足够清楚.如有必要,我可以提供更多细节.

FactoryGen.h

#pragma once

#include <map>
#include <tuple>
#include <typeinfo>

//C++11 typename aliasing, doesn't work in visual studio though...
/*
template<typename Base>
using FactoryGen<Base> = FactoryGen<Base,void>;
*/

//Assign unique ids to all classes within this map.  Better than typeid(class).hash_code() since there is no computation during run-time.
size_t __CLASS_UID = 0;

template<typename T>
inline size_t __GET_CLASS_UID(){
    static const size_t id = __CLASS_UID++;
    return id;
}

//These are the common code snippets from the factories and their specializations. 
template<typename Base>
struct FactoryGenCommon{
    typedef std::pair<void*,size_t> Factory; //A factory is a function pointer and its unique type identifier

    //Generates the function pointer type so that I don't have stupid looking typedefs everywhere
    template<typename... InArgs>
    struct FPInfo{ //stands for "Function Pointer Information"
        typedef Base* (*Type)(InArgs...);
    };

    //Check to see if a Factory is not null and matches it's signature (helps make sure a factory actually takes the specified inputs)
    template<typename... InArgs>
    static bool isValid(const Factory& factory){
        auto maker = factory.first;
        if(maker==nullptr) return false;

        //we have to check if the Factory will take those inArgs
        auto type = factory.second;
        auto intype = __GET_CLASS_UID<FPInfo<InArgs...>>();
        if(intype != type) return false;

        return true;
    }
};

//template inputs are the Base type for which the factory returns, and the Args... that will determine how the function pointers are indexed.
template<typename Base, typename... Args> 
struct FactoryGen : FactoryGenCommon<Base>{
    typedef std::tuple<Args...> Tuple;
    typedef std::map<Tuple,Factory> Map; //the Args... are keys to a map of function pointers

    inline static Map& get(){ 
        static Map factoryMap;
        return factoryMap; 
    }

    template<typename... InArgs>
    static void add(void* factory, const Args&... args){
        Tuple selTuple = std::make_tuple(args...); //selTuple means Selecting Tuple.  This Tuple is the key to the map that gives us a function pointer
        get()[selTuple] = Factory(factory,__GET_CLASS_UID<FPInfo<InArgs...>>());
    }

    template<typename... InArgs>
    static Base* make(const Args&... args, const InArgs&... inArgs){
        Factory factory = get()[std::make_tuple(args...)];
        if(!isValid<InArgs...>(factory)) return nullptr;
        return ((FPInfo<InArgs...>::Type)factory.first) (inArgs...);
    }
};

//Specialize for factories with no selection mapping
template<typename Base>
struct FactoryGen<Base,void> : FactoryGenCommon<Base>{
    inline static Factory& get(){
        static Factory factory;
        return factory; 
    }

    template<typename... InArgs>
    static void add(void* factory){
        get() = Factory(factory,__GET_CLASS_UID<FPInfo<InArgs...>>());
    }

    template<typename... InArgs>
    static Base* make(const InArgs&... inArgs){
        Factory factory = get();
        if(!isValid<InArgs...>(factory)) return nullptr;
        return ((FPInfo<InArgs...>::Type)factory.first) (inArgs...);
    }
};

//this calls the function "initialize()" function to register each class ONCE with the respective factory (even if a class tries to initialize multiple times)
//this step can probably be circumvented, but I'm not totally sure how
template <class T>
class RegisterInit {
  int& count(void) { static int x = 0; return x; } //counts the number of callers per derived
public:
  RegisterInit(void) { 
    if ((count())++ == 0) { //only initialize on the first caller of that class T
      T::initialize();
    }
  }
};
Run Code Online (Sandbox Code Playgroud)

Base.h

#pragma once

#include <map>
#include <string>
#include <iostream>
#include "Procedure.h"
#include "FactoryGen.h"

class Base {
public:
    static Base* makeBase(){ return new Base; }
    static void initialize(){ FactoryGen<Base,void>::add(Base::makeBase); } //we want this to be the default mapping, specify that it takes void inputs

    virtual void speak(){ std::cout << "Base" << std::endl; }
};

RegisterInit<Base> __Base; //calls initialize for Base
Run Code Online (Sandbox Code Playgroud)

Derived.h

#pragma once

#include "Base.h"

class Derived0 : public Base {
private:
    std::string speakStr;
public:
    Derived0(std::string sayThis){ speakStr=sayThis; }

    static Base* make(std::string sayThis){ return new Derived0(sayThis); }
    static void initialize(){ FactoryGen<Base,int>::add<std::string>(Derived0::make,0); } //we map to this subclass via int with 0, but specify that it takes a string input

    virtual void speak(){ std::cout << speakStr << std::endl; }
};

RegisterInit<Derived0> __d0init; //calls initialize() for Derived0

class Derived1 : public Base {
private:
    std::string speakStr;
public:
    Derived1(std::string sayThis){ speakStr=sayThis; }

    static Base* make(std::string sayThat){ return new Derived0(sayThat); }
    static void initialize(){ FactoryGen<Base,int>::add<std::string>(Derived0::make,1); } //we map to this subclass via int with 1, but specify that it takes a string input

    virtual void speak(){ std::cout << speakStr << std::endl; }
};

RegisterInit<Derived1> __d1init; //calls initialize() for Derived1
Run Code Online (Sandbox Code Playgroud)

Main.cpp的

#include <windows.h> //for Sleep()
#include "Base.h"
#include "Derived.h"

using namespace std;

int main(){
    Base* b = FactoryGen<Base,void>::make(); //no mapping, no inputs
    Base* d0 = FactoryGen<Base,int>::make<string>(0,"Derived0"); //int mapping, string input
    Base* d1 = FactoryGen<Base,int>::make<string>(1,"I am Derived1"); //int mapping, string input

    b->speak();
    d0->speak();
    d1->speak();

    cout << "Size of Base: " << sizeof(Base) << endl;
    cout << "Size of Derived0: " << sizeof(Derived0) << endl;

    Sleep(3000); //Windows & Visual Studio, sry
}
Run Code Online (Sandbox Code Playgroud)

我认为这是一个非常灵活/可扩展的工厂库.虽然它的代码不是非常直观,但我认为使用它非常简单.当然,我的看法是有偏见的,因为我是写作的人,所以如果相反,请告诉我.

编辑:清理FactoryGen.h文件.这可能是我的最后一次更新,但这是一个有趣的练习.


Ale*_*eph 6

我的评论可能不太清楚.所以这里是一个依赖于模板元编程的C++ 11"解决方案":(可能不是最好的方法)

#include <iostream>
#include <utility>


// Type list stuff: (perhaps use an existing library here)
class EmptyType {};

template<class T1, class T2 = EmptyType>
struct TypeList
{
    typedef T1 Head;
    typedef T2 Tail;
};

template<class... Etc>
struct MakeTypeList;

template <class Head>
struct MakeTypeList<Head>
{
    typedef TypeList<Head> Type;
};

template <class Head, class... Etc>
struct MakeTypeList<Head, Etc...>
{
    typedef TypeList<Head, typename MakeTypeList<Etc...>::Type > Type;
};

// Calling produce
template<class TList, class BaseType>
struct Producer;

template<class BaseType>
struct Producer<EmptyType, BaseType>
{
    template<class... Args>
    static BaseType* Produce(Args... args)
    {
        return nullptr;
    }
};

template<class Head, class Tail, class BaseType>
struct Producer<TypeList<Head, Tail>, BaseType>
{
    template<class... Args>
    static BaseType* Produce(Args... args)
    {
        BaseType* b = Head::Produce(args...);
        if(b != nullptr)
            return b;
        return Producer<Tail, BaseType>::Produce(args...);
    }
};

// Generic AbstractFactory:
template<class BaseType, class Types>
struct AbstractFactory {
    typedef Producer<Types, BaseType> ProducerType;

    template<class... Args>
    static BaseType* Produce(Args... args)
    {
        return ProducerType::Produce(args...);
    }
};

class Base {}; // Example base class you had

struct Derived0 : public Base { // Example derived class you had
    Derived0() = default;
    static Base* Produce(int value)
    {
        if(value == 0)
            return new Derived0();
        return nullptr;
    }
};

struct Derived1 : public Base { // Another example class
    Derived1() = default;
    static Base* Produce(int value)
    {
        if(value == 1)
            return new Derived1();
        return nullptr;
    }
};

int main()
{
    // This will be our abstract factory type:
    typedef AbstractFactory<Base, MakeTypeList<Derived0, Derived1>::Type> Factory;
    Base* b1 = Factory::Produce(1);
    Base* b0 = Factory::Produce(0);
    Base* b2 = Factory::Produce(2);
    // As expected b2 is nullptr
    std::cout << b0 << ", " << b1 << ", " << b2 << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

好处:

  1. 没有(额外的)运行时开销,就像使用函数指针一样.适用于任何基类型和任意数量的派生类型.你最终还是会调用这些功能.
  2. 由于可变参数模板,这适用于任意数量的参数(给出不正确数量的参数将产生编译时错误消息).
  3. 不需要显式注册产品成员函数.

缺点:

  1. 声明Factory类型时,所有派生类型都必须可用.(您必须知道可能的派生类型是什么,它们必须完整.)
  2. 派生类型的产品成员函数必须是公共的.
  3. 可以使编译速度变慢.(与依赖模板元编程的情况一样)

最后,使用原型设计模式可能会变得更好.我不知道,因为我没有尝试过使用我的代码.

我想陈述一些额外的事情(在进一步讨论聊天之后):

  • 每个工厂只能返回一个对象.这看起来很奇怪,因为用户决定他们是否会接受输入来创建他们的对象.因此我建议您的工厂可以返回一组对象.
  • 小心不要使事情过于复杂.你想要一个插件系统,但我不认为你真的想要工厂.我建议你只需让用户注册他们的类(在他们的共享对象中),你只需将参数传递给类的Produce(静态)成员函数.当且仅当它们不是nullptr时才存储对象.