24

で特定の基本クラスのサブクラスのファクトリを実装する直感的で拡張可能な方法を探しています。ライブラリでそのようなファクトリ関数を提供したいのですが、トリッキーな部分は、そのファクトリがユーザー定義のサブクラスでも機能するようにすることです(たとえば、ライブラリのファクトリ関数に、リンクされているモジュールに応じて異なるサブクラスを構築させる)。目標は、下流の開発者がファクトリを使用する際の負担/混乱を最小限に抑えることです。

私がやりたいことの例は次のとおりです。 を指定するstd::istreamと、コンテンツに一致するサブクラスのオブジェクトを構築して返すか、一致するものが見つからない場合は null ポインターを返します。グローバル ファクトリには、次のような署名があります。

Base* Factory(std::istream &is){ ... };

私はプロトタイプ ファクトリに精通していますが、プロトタイプ オブジェクトを作成/保存する必要は避けたいと考えています。に関する関連する質問がここに投稿されています: Allowing maximumflexively/extensibility using a factory .

固有のソリューションを探しているわけではありませんが、よりエレガントなソリューションがあれば、喜んで学びたいと思います。

私はかなりエレガントだと思う1つの実用的なソリューションを思いつきました。これを回答として投稿します。この問題はかなり一般的であると想像できるので、誰かがより良いアプローチを知っているかどうか疑問に思っています.

編集:いくつかの説明が必要なようです...

アイデアは、ファクトリが派生クラスのオブジェクトを構築することであり、どちらを決定するかを決定するロジックは含まれていません。さらに悪いことに、ファクトリ メソッドは最終的にライブラリの一部になり、派生クラスはプラグインで定義される可能性があります。

派生クラスは、提供された入力 (入力ファイルなど) に基づいて、構築に適しているかどうかを自分で判断できなければなりません。この決定は、何人かの人々によって提案されたように、ファクトリで使用できる述語として実装できます (ちなみに、素晴らしい提案です!)。

4

7 に答える 7

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;
}

利点:

  1. 関数ポインターの場合のような (追加の) 実行時のオーバーヘッドはありません。任意の基本型と、任意の数の派生型で機能します。もちろん、関数を呼び出すことになります。
  2. 可変個引数テンプレートのおかげで、これは任意の数の引数で機能します (引数の数が正しくないと、コンパイル時にエラー メッセージが生成されます)。
  3. 生成メンバー関数の明示的な登録は必要ありません。

短所:

  1. Factory 型を宣言するときは、すべての派生型を使用できる必要があります。(可能な派生型が何であるかを知っている必要があり、それらは完全である必要があります。)
  2. 派生型の生成メンバー関数はパブリックである必要があります。
  3. コンパイルが遅くなる可能性があります。(テンプレートのメタプログラミングに依存する場合は常にそうです)

最終的には、プロトタイプのデザイン パターンを使用した方がよい結果が得られる可能性があります。コードを使ってみたことがないのでわかりません。

いくつかの追加事項を述べたいと思います(チャットでさらに議論した後):

  • 各ファクトリは単一のオブジェクトのみを返すことができます。オブジェクトを作成するために入力を受け取るかどうかをユーザーが決定するため、これは奇妙に思えます。そのため、代わりにファクトリがオブジェクトのコレクションを返すことができることをお勧めします。
  • 物事を複雑にしすぎないように注意してください。プラグイン システムは必要ですが、工場は必要ないと思います。ユーザーに自分のクラスを (共有オブジェクトに) 登録させ、引数をクラスのProduce(静的) メンバー関数に渡すだけにすることをお勧めします。オブジェクトが nullptr でない場合にのみ、オブジェクトを保存します。
于 2013-07-01T12:43:36.687 に答える
4

更新:この回答は、読み取って工場に渡すことができるある種の魔法が存在することを前提としていましたが、明らかにそうではありません。a) 更新するかもしれないし、b) とにかく気に入っているからです。


C ++ 11の手法を使用していない(まだ更新する機会がなかったり、スマートポインターを返すなどの機会がなかった)、完全に自分の仕事ではありませんが、これは私が使用するファクトリークラス。重要なことに(IMHO)、一致するものを見つけるために可能な各クラスのメソッドを呼び出すのではなく、マップを介してこれを行います。

#include <map>
// extraneous code has been removed, such as empty constructors, ...
template <typename _Key, typename _Base, typename _Pred = std::less<_Key> >
class Factory {
public:
    typedef _Base* (*CreatorFunction) (void);
    typedef std::map<_Key, CreatorFunction, _Pred> _mapFactory;

    // called statically by all classes that can be created
    static _Key Register(_Key idKey, CreatorFunction classCreator) {
        get_mapFactory()->insert(std::pair<_Key, CreatorFunction>(idKey, classCreator));
        return idKey;
    }

    // Tries to create instance based on the key
    static _Base* Create(_Key idKey) {
        _mapFactory::iterator it = get_mapFactory()->find(idKey);
        if (it != get_mapFactory()->end()) {
            if (it->second) {
                return it->second();
            }
        }
        return 0;
    }

protected:
    static _mapFactory * get_mapFactory() {
        static _mapFactory m_sMapFactory;
        return &m_sMapFactory;
    }
};

これを使用するには、基本型を宣言し、クラスごとに静的として登録します。登録するとキーが返されるので、これをクラスのメンバーとして追加する傾向がありますが、必ずしも必要ではありません:) ...

// shape.h
// extraneous code has been removed, such as empty constructors, ...
// we also don't technically need the id() method, but it could be handy
// if at a later point you wish to query the type.
class Shape {
public:
    virtual std::string id() const = 0;
};
typedef Factory<std::string, Shape> TShapeFactory;

これで、新しい派生クラスを作成し、それを作成可能として登録できますTShapeFactory...

// cube.h
// extraneous code has been removed, such as empty constructors, ...
class Cube : public Shape {
protected:
    static const std::string _id;
public:
    static Shape* Create() {return new Cube;}
    virtual std::string id() const {return _id;};
};

// cube.cpp
const std::string Cube::_id = TShapeFactory::Register("cube", Cube::Create);

次に、この場合は文字列に基づいて新しいアイテムを作成できます。

Shape* a_cube = TShapeFactory::Create("cube");
Shape* a_triangle = TShapeFactory::Create("triangle");
// a_triangle is a null pointer, as we've not registered a "triangle"

この方法の利点は、ファクトリで生成可能な新しい派生クラスを作成する場合、ファクトリ クラスを確認してベースから派生させることができれば、他のコードを変更する必要がないことです。

// sphere.h
// extraneous code has been removed, such as empty constructors, ...
class Sphere : public Shape {
protected:
    static const std::string _id;
public:
    static Shape* Create() {return new Sphere;}
    virtual std::string id() const {return _id;};
};

// sphere.cpp
const std::string Sphere::_id = TShapeFactory::Register("sphere", Sphere::Create);

読者に任せる改善点としては、次のようなものを追加することが含まtypedef _Base base_classFactoryますTShapeFactory::base_class。Factory はおそらくキーが既に存在するかどうかも確認する必要がありますが、これも演習として残されています。

于 2013-07-01T15:35:08.640 に答える
3

これは、実行時に解決されるファクトリを管理するための持続可能なイディオムです。私は過去にこれを使用して、かなり洗練された動作をサポートしました。私は、機能性をあまり犠牲にすることなく、シンプルさと保守性を重視しています。

TLDR:

  • 一般的に静的初期化を避ける
  • ペストのような「自動読み込み」テクニックを避ける
  • オブジェクトとファクトリの所有権を伝達する
  • 使い分けと工場管理の悩み

ランタイム ファクトリの使用

これは、このファクトリ システムのユーザーが操作する基本インターフェイスです。工場の詳細について心配する必要はありません。

class BaseObject {
public:
    virtual ~BaseObject() {}
};

BaseObject* CreateObjectFromStream(std::istream& is);

余談ですが、生のポインターの代わりに参照、、boost::optionalまたはを使用することをお勧めします。shared_ptr完璧な世界では、このオブジェクトの所有者をインターフェイスが教えてくれるはずです。ユーザーとして、このポインターが与えられたとき削除する責任はありますか? それがshared_ptr.

ランタイム ファクトリの実装

別のヘッダーに、ファクトリがアクティブな場合のスコープの管理の詳細を入れます。

class RuntimeFactory {
public:
    virtual BaseObject* create(std::istream& is) = 0;
};

void RegisterRuntimeFactory(RuntimeFactory* factory);
void UnregisterRuntimeFactory(RuntimeFactory* factory);

これらすべての重要な点は、ファクトリがどのように初期化および使用されるかとは異なる問題であるということです。

これらの無料関数の呼び出し元がファクトリを所有していることに注意してください。レジストリはそれらを所有していません。

これは厳密には必要ではありませんが、これらの工場がいつどこで破壊されるかをより詳細に制御できます。重要なのは、「作成後」または「破棄前」の呼び出しなどを目にするときです。このような名前のファクトリ メソッドは、所有権の逆転の設計上の匂いです。

とにかく、ファクトリのライフタイムを管理するためにこれに別のラッパーを書くのは簡単です。また、構成にも役立ちます。これは優れています。

新しい工場を登録する

ファクトリ登録ごとにラッパーを作成します。私は通常、各工場登録を独自のヘッダーに入れます。これらのヘッダーは通常、2 つの関数呼び出しだけです。

void RegisterFooFactory();
void UnregisterFooFactory();

これはやり過ぎのように思えるかもしれませんが、この種の勤勉さによってコンパイル時間が短縮されます。

私のmainその後は、一連の登録および登録解除の呼び出しに削減されます。

#include <foo_register.h>
#include <bar_register.h>

int main(int argc, char* argv[]) {
    SetupLogging();
    SetupRuntimeFactory();
    RegisterFooFactory();
    RegisterBarFactory();

    // do work...

    UnregisterFooFactory();
    UnregisterBarFactory();
    CleanupLogging();
    return 0;
}

静的初期化の落とし穴を避ける

これにより、他のソリューションのように静的読み込み中に作成されたオブジェクトが特に回避されます。これは事故 ではありません。

  • C++ の仕様では、静的な読み込みがいつ発生するかについて有用な保証が得られません。
  • 何か問題が発生すると、スタック トレースが取得されます
  • コードはシンプルで、直接的で、従うのが簡単です

レジストリの実装

ご想像のとおり、実装の詳細はかなりありふれたものです。

class RuntimeFactoryRegistry {
public:
    void registerFactory(RuntimeFactory* factory) {
        factories.insert(factory);
    }

    void unregisterFactory(RuntimeFactory* factory) {
        factories.erase(factory);
    }

    BaseObject* create(std::istream& is) {
        std::set<RuntimeFactory*>::iterator cur = factories.begin();
        std::set<RuntimeFactory*>::iterator end = factories.end();
        for (; cur != end; cur++) {
            // reset input?
            if (BaseObject* obj = (*cur)->create(is)) {
                return obj;
            }
        }
        return 0;
    }

private:
    std::set<RuntimeFactory*> factories;
};

これは、すべてのファクトリが相互に排他的であることを前提としています。この仮定を緩和しても、ソフトウェアが適切に動作することはまずありません。私はおそらく個人的により強い主張をするでしょう、へへ。もう 1 つの方法は、オブジェクトのリストを返すことです。

以下の実装は、デモを簡単にするために静的です。これは、マルチスレッド環境では問題になる可能性があります。静的である必要はありません。また、静的であるべきかどうかもお勧めしません。ここにあるだけです。議論の本題ではないので、このままにしておきます。

これらのフリー関数は、この実装のパススルー関数としてのみ機能します。これにより、レジストリを単体テストしたり、必要に応じて再利用したりできます。

namespace {

    static RuntimeFactoryRegistry* registry = 0;

} // anon    

void SetupRuntimeFactory() {
    registry = new RuntimeFactoryRegistry;
}

void CleanupRuntimeFactory() {
    delete registry;
    registry = 0;
}

BaseObject* CreateObjectFromStream(std::istream& is) {
    return registry->create(is);
}

void RegisterRuntimeFactory(RuntimeFactory* factory) {
    registry->registerFactory(factory);
}

void UnregisterRuntimeFactory(RuntimeFactory* factory) {
    registry->unregisterFactory(factory);
}
于 2013-07-03T02:05:43.683 に答える
3

現在考えられる最善の解決策はFactory、各派生クラスの生成関数へのポインターを格納するクラスを使用することです。新しい派生クラスが作成されると、生成メソッドへの関数ポインタをファクトリに格納できます。

私のアプローチを説明するためのコードを次に示します。

#include <iostream>
#include <vector>

class Base{};

// Factory class to produce Base* objects from an int (for simplicity).
// The class uses a list of registered function pointers, which attempt
// to produce a derived class based on the given int.
class Factory{
public:
    typedef Base*(*ReadFunPtr)(int);
private:
    static vector<ReadFunPtr> registeredFuns;
public:
    static void registerPtr(ReadFunPtr ptr){ registeredFuns.push_back(ptr); }
    static Base* Produce(int value){
        Base *ptr=NULL;
        for(vector<ReadFunPtr>::const_iterator I=registeredFuns.begin(),E=registeredFuns.end();I!=E;++I){
            ptr=(*I)(value);
            if(ptr!=NULL){
                return ptr;
            }
        }
        return NULL;
    }
};
// initialize vector of funptrs
std::vector<Factory::ReadFunPtr> Factory::registeredFuns=std::vector<Factory::ReadFunPtr>();

// An example Derived class, which can be produced from an int=0. 
// The producing method is static to avoid the need for prototype objects.
class Derived : public Base{
    private:
        static Base* ProduceDerivedFromInt(int value){ 
            if(value==0) return new Derived();
            return NULL;
        }
public:
    Derived(){};

    // registrar is a friend because we made the producing function private
    // this is not necessary, may be desirable (e.g. encapsulation)
    friend class DerivedRegistrar;
};

// Register Derived in the Factory so it will attempt to construct objects.
// This is done by adding the function pointer Derived::ProduceDerivedFromInt
// in the Factory's list of registered functions.
struct DerivedRegistrar{ 
    DerivedRegistrar(){ 
        Factory::registerPtr(&(Derived::ProduceDerivedFromInt));
    }
} derivedregistrar;

int main(){
    // attempt to produce a Derived object from 1: should fail
    Base* test=Factory::Produce(1);
    std::cout << test << std::endl; // outputs 0

    // attempt to produce a Derived object from 0: works
    test=Factory::Produce(0);
    std::cout << test << std::endl; // outputs an address
}

TL;DR : このアプローチでは、下流の開発者は、派生クラスの生成関数をstaticメンバー関数 (または非メンバー関数) として実装し、単純な を使用してファクトリに登録する必要がありstructます。

これは簡単そうに見え、プロトタイプ オブジェクトは必要ありません。

于 2013-06-29T09:57:02.207 に答える
1

まず、ここには意見を述べるのに十分な詳細がないため、推測に任せます。挑戦的な質問と最小限の解決策を提供しましたが、解決策の何が問題なのかを明確にしませんでした。

苦情は、拒否された建設と次の建設の試みの間で何も知らないというリセットに集中していると思われます。非常に多数の潜在的な工場がある場合、このリセットにより、同じデータを何百回または何千回も解析する可能性があります。これが問題である場合、問題は次のとおりです。述語評価フェーズをどのように構築して、作業量を制限し、以前の解析結果を再利用できるようにしますか。

各ファクトリを次のように登録することをお勧めします: 1) 特殊化パラメータを取るファクトリ ビルダー関数 (例では iostream) 2) ブール述語の順序付けられていないセット 3) 構築を可能にするために各述語のブール値が必要

述語のセットは、述語ツリーを作成/変更するために使用されます。ツリーの内部ノードは、述語 (「合格」、「不合格」、場合によっては「ドントケア」への分岐) を表します。内部ノードとリーフの両方がコンストラクターを保持し、祖先の述語が満たされている場合に満たされます。ツリーをたどると、まず現在のレベルでコンストラクターを探し、次に述語を評価して、必要なパスをたどります。その子パスに沿って解決策が見つからない場合は、「ドント ケア」パスに従います。

これにより、新しいファクトリが述語関数を共有できるようになります。工場がオンライン/オフラインになったときのツリーの管理/ソートについて、おそらく多くの質問があります。また、述語全体で保持し、構築が完了したときにリセットする必要があるパーサー状態データの可能性もあります。未解決の問題がたくさんありますが、これは、ソリューションで認識されている問題に対処するのに役立つ場合があります。

TL:DR; 構築を試みるときにトラバースする述語のグラフを作成します。

于 2013-07-03T06:37:54.777 に答える