注釈
これは、生成的なデザインパターンと関連する問題について啓発された3番目の記事です。 ここでは、オブジェクト、ファクトリー、
ファクトリー 、抽象ファクトリー、ビルダー、プロトタイプ、マルチトーン、遅延初期化、およびpimplイディオムまたはテンプレート「ブリッジ」へのちょっとしたタッチを作成するためのお気に入りのテクニックを見ていきます。
シングルトーンの使用については、
最初の[1]および
2番目の[2]の記事で詳しく説明しましたが、後で説明するように、
シングルトーンは他のデザインパターンと組み合わせて使用されることがよくあります。
はじめに
おそらく多くの人が、
生成的なデザインパターンを聞いたり、読んだり、使用したりしました
[4] 。 この記事では、それらについてのみ説明します。 ただし、ここでは他のことに重点を置きます。 もちろん、この記事は生成パターンのガイドとして、またはそれらの紹介として使用できます。 しかし、私の最終的な目標は、わずかに異なる面、つまり、これらのテンプレートを実際のコードで使用する面です。
テンプレートについて学んだ多くの人がどこでもテンプレートを使い始めようとするのは秘密ではありません。 ただし、すべてがそれほど単純ではありません。 このトピックに関する多くの記事は、コードでの使用に十分な注意を払っていません。 そして、テンプレートをコードに固定し始めると、おとぎ話でもペンでも説明できないほど想像を絶する何かが生じます。 私はこれらのアイデアのさまざまな化身を見てきましたが、時々自発的に質問をします。著者は何を吸っていたのですか? たとえば、
Wikipedia [3]のファクトリーまたは
ファクトリー・メソッドを取り上げます。 すべてのコードを提供するのではなく、次のもののみを使用します。
const size_t count = 2;
実際の生活でそれをどのように使用するかを自問すると、すぐに次の発言が起こります:
- 0番目または1番目の要素を使用する必要があることをどのようにして知ることができますか? 違いはありません。
- ループ内にいくつかの要素を作成する必要があるとします。 これらの工場の所在地に関する情報はどこで入手できますか? すぐに工場を初期化する場合、なぜそれらが必要なのですか? オブジェクトを作成し、すべてを実行する特定のメソッドまたはスタンドアロン関数を呼び出すことができます。
- オブジェクトは、newオペレーターによって作成されると想定されています。 これにより、例外的な状況の処理とオブジェクトの存続期間の問題がすぐに発生します。
好むと好まざるとにかかわらず、この例は、多くの欠陥を含む一種の説明にすぎません。 実際には、これは使用されません。
「それから何が使われますか?」、注意深い読者は尋ねます。 以下に使用コードを示します。 このリストは完全なものではありません。
「実生活」の工場は通常シングルトンであることは注目に値します。 また、オブジェクトを作成するときに、使用したパターンから耳が突き出ていることにも気付くことができます。 その後のリファクタリングにより、これは不快な側面から感じられます。 オブジェクトをポインターで返す場合、アプローチがよく使用されます。 すべての本で教えられているように、コードは書かれ続けています。 createObjectメソッドですべてが明確な場合-最後にdeleteを呼び出す必要があります。その後、設定をどうしますか? シングルトンかどうか? もしそうなら、何もする必要はありません。 そうでない場合は? 繰り返しになりますが、生涯にわたって疑問が生じます。 例外を正しく処理することを忘れないでください。例外処理を使用したこのようなコードは、リソースのクリーニングに関連する問題を引き起こします。
好むと好まざるとにかかわらず、生成されたオブジェクトを赤いスレッドで処理し、さまざまな作成方法を区別しない統一的なアプローチが必要です。 これを実装するために
、依存関係反転の強力な
原則を使用します
[7] 。 その本質は、特定の抽象化、インターフェースが導入されていることです。 さらに、使用コードと使用コードは、導入されたインターフェイスを介して、たとえば
制御反転[8]を使用して接続されます。 これにより、オブジェクトを作成するコードは、クラス作成の詳細から抽象化し、単純に専用インターフェイスを使用することができます。 すべての注意は、このインターフェイスを実装する機能の肩にかかっています。 この記事では、ほとんどすべての既知の生成デザインパターンを使用してオブジェクトを作成する方法と、複数の生成パターンを使用して同時にインスタンスを作成する例について詳しく説明します。 シングルトンの例は、
以前の記事[2]で詳細に説明されて
います 。この記事では、他のテンプレートでのみそれを使用します。
インフラ
Object Anとその周辺のインフラストラクチャについては、
2番目の記事[2]で詳しく説明しています。 ここでは、後続のナレーションで使用されるコードのみを提供します。 詳細については、
以前の記事[2]を参照してください。
template<typename T> struct An { template<typename U> friend struct An; An() {} template<typename U> An(const An<U>& a) : data(a.data) {} template<typename U> An(An<U>&& a) : data(std::move(a.data)) {} T* operator->() { return get0(); } const T* operator->() const { return get0(); } bool isEmpty() const { return !data; } void clear() { data.reset(); } void init() { if (!data) reinit(); } void reinit() { anFill(*this); } T& create() { return create<T>(); } template<typename U> U& create() { U* u = new U; data.reset(u); return *u; } template<typename U> void produce(U&& u) { anProduce(*this, u); } template<typename U> void copy(const An<U>& a) { data.reset(new U(*a.data)); } private: T* get0() const { const_cast<An*>(this)->init(); return data.get(); } std::shared_ptr<T> data; }; template<typename T> void anFill(An<T>& a) { throw std::runtime_error(std::string("Cannot find implementation for interface: ") + typeid(T).name()); } template<typename T> struct AnAutoCreate : An<T> { AnAutoCreate() { create(); } }; template<typename T> T& single() { static T t; return t; } template<typename T> An<T> anSingle() { return single<AnAutoCreate<T>>(); } #define PROTO_IFACE(D_iface, D_an) \ template<> void anFill<D_iface>(An<D_iface>& D_an) #define DECLARE_IMPL(D_iface) \ PROTO_IFACE(D_iface, a); #define BIND_TO_IMPL(D_iface, D_impl) \ PROTO_IFACE(D_iface, a) { a.create<D_impl>(); } #define BIND_TO_SELF(D_impl) \ BIND_TO_IMPL(D_impl, D_impl) #define BIND_TO_IMPL_SINGLE(D_iface, D_impl) \ PROTO_IFACE(D_iface, a) { a = anSingle<D_impl>(); } #define BIND_TO_SELF_SINGLE(D_impl) \ BIND_TO_IMPL_SINGLE(D_impl, D_impl) #define BIND_TO_IFACE(D_iface, D_ifaceFrom) \ PROTO_IFACE(D_iface, a) { anFill<D_ifaceFrom>(a); } #define BIND_TO_PROTOTYPE(D_iface, D_prototype) \ PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }
つまり、Anオブジェクトは「スマート」ポインターであり、anFill関数を使用してアクセスすると自動的に設定されます。 必要なインターフェイスにこの関数をオーバーロードします。 入力データに基づいてオブジェクトを作成するには、anProduce関数を使用します。その使用方法については、ファクトリーのセクションで説明します。
ブリッジテンプレート
最も単純で最も一般的なケースから始めましょう。オブジェクトデータを非表示にし、使用するインターフェイスのみを残します。 したがって、たとえば、1つのフィールドをクラスに追加するなど、データを変更する場合、このクラスを使用するすべてを再コンパイルする必要はありません。 この設計パターンは「ブリッジ」と呼ばれ、ポンポンのイディオムについても話します。 このアプローチは、多くの場合、インターフェイスを実装から分離するために使用されます。
まず、各抽象クラスに仮想デストラクタを記述しないように、IObjectクラスを作成します。 次に、IObjectから各インターフェイス(抽象クラス)を単純に継承します。 IFruitインターフェースには、アプローチを説明する単一のgetName()関数が含まれています。 宣言全体がヘッダーファイルで行われます。 特定の実装は、すでにcppファイルに書き込まれています。 ここでgetName()関数を定義し、インターフェイスを実装にバインドします。 Orangeクラスへの変更を変更するときは、1つのファイルを再コンパイルするだけです。
の使用を見てみましょう:
An<IFruit> f; std::cout << "Name: " << f->getName() << std::endl;
ここでは、単にAnオブジェクトを作成し、最初のアクセス時に、cppファイルに記述されている目的の実装でオブジェクトが作成されます。 寿命は自動的に制御されます。 関数を終了すると、オブジェクトは自動的に破棄されます。
工場テンプレート
次に、最も一般的なパターンについて説明しましょう。ファクトリメソッドまたはファクトリだけです。 ここでは、ファクトリが通常どのように使用されるかの例を示しません。 これは、たとえば
Wikipediaで読むことができます。 このデザインパターンのわずかに異なる使用方法を示します。
使用法の違いは、ほとんどの場合、ユーザーには見えないということです。 しかし、これは制限があるという意味ではありません。 この記事では、提案されたアプローチの柔軟性と強度を実証します。
このために、問題を提起します。関数の入力パラメーターに応じてさまざまなオブジェクトを作成する必要があります。 一般的に言えば、生成関数はいくつかのパラメーターを持つことができます。 ただし、一般性を失うことなく、いくつかのパラメーターを持つ関数は、必要な入力データを持つ構造体が引数として使用される1つのパラメーターを持つ関数に還元できると想定できます。 したがって、インターフェイスと理解を簡素化するために、どこでもどこでも1つのパラメーターを持つ関数を使用します。 興味のある人は、新しいc ++ 0x標準の可変テンプレートを使用できますが、残念ながらmsvcおよびiccコンパイラはまだサポートしていません。
そのため、フルーツFruitTypeのタイプに応じて、IFruitインターフェイスの実装を作成するタスクに直面しています。
enum FruitType { FT_ORANGE, FT_APPLE };
これを行うには、Appleの追加の実装が必要です。
生成関数を作成します。
void anProduce(An<IFruit>& a, FruitType type) { switch (type) { case FT_ORANGE: a.create<Orange>(); break; case FT_APPLE: a.create<Apple>(); break; default: throw std::runtime_error("Unknown fruit type"); } }
この関数は、以下に示すように、An :: produceメソッドが呼び出されると自動的に呼び出されます。
An<IFruit> f; f.produce(FT_ORANGE); std::cout << f->getName() << std::endl; f.produce(FT_APPLE); std::cout << f->getName() << std::endl;
多くの場合、非ランタイム値に応じてオブジェクトを作成すると便利です。 いつでも、どのオブジェクトを作成したいかが明確にわかります。 その後、他のより簡単な方法で作成できます。 最初の方法は、中間オブジェクトを作成することです-タグ:
2番目のオプションは、特別なインターフェイスを作成し、「ブリッジ」テンプレートを使用することです。
ビルダーテンプレート
多くの人(自分を含む)は困惑していますが、工場があるのになぜビルダーが必要なのでしょうか? 実際、実際には、これらは同様のパターンです。 それらはどう違うのですか?
それらを次の単純な方法で明確に区別します。ファクトリーはインスタンスの作成に使用され、そのタイプは渡されるパラメーターに依存します。 タイプがわかっているときにビルダーが使用されますが、オブジェクトのフィールドに入力する方法はさまざまです。 つまり ファクトリーは異なるタイプを作成します。ビルダーの場合、同じタイプが使用されますが、内容は異なります。 それでは、例から始めましょう。
ここには、Fruitクラスがありますが、これはもはや抽象ではありません。 使い慣れたgetName()メソッドが含まれています。このメソッドは、単にクラスのコンテンツから目的のタイプを抽出します。 ビルダーのタスクは、このフィールドに正しく入力することです。 このために、2つのクラスが使用され、デザイナーはこのフィールドに正しい値を入力します。 生成関数anProduceは必要なインスタンスを作成し、そのコンストラクターは必要なすべての作業を行います。
An<Fruit> f; f.produce(FT_ORANGE); std::cout << f->getName() << std::endl; f.produce(FT_APPLE); std::cout << f->getName() << std::endl;
抽象ファクトリーテンプレート
このテンプレートは、特定の関係を持つオブジェクトのセットを作成する必要がある場合に使用されます。 このアプローチを説明するために、次の例を検討してください。
GUIオブジェクトを作成する必要があるとします:
struct IWindow : IObject { virtual std::string getWindowName() = 0; }; struct IButton : IObject { virtual std::string getButtonName() = 0; };
同時に、このようなオブジェクトを操作できるフレームワークがいくつかあります。そのうちの1つは、たとえばgtkです。 これを行うには、オブジェクトを生成するためのインターフェイスを作成します。
struct IWindowsManager : IObject { virtual void produceWindow(An<IWindow>& a) = 0; virtual void produceButton(An<IButton>& a) = 0; };
次に、実装を宣言します。
struct GtkWindow : IWindow { virtual std::string getWindowName() { return "GtkWindow"; } }; struct GtkButton : IButton { virtual std::string getButtonName() { return "GtkButton"; } }; struct GtkWindowsManager : IWindowsManager { virtual void produceWindow(An<IWindow>& a) { a.create<GtkWindow>(); } virtual void produceButton(An<IButton>& a) { a.create<GtkButton>(); } }; BIND_TO_IMPL_SINGLE(IWindowsManager, GtkWindowsManager)
そして、生成関数を作成します。
PROTO_IFACE(IWindow, a) { An<IWindowsManager> pwm; pwm->produceWindow(a); } PROTO_IFACE(IButton, a) { An<IWindowsManager> pwm; pwm->produceButton(a); }
これで、インターフェイスを使用できます。
An<IButton> b; std::cout << b->getWindowName() << std::endl; An<IWindow> w; std::cout << w->getButtonName() << std::endl;
例を複雑にしましょう。 構成に応じてフレームワークを選択する必要があるとしましょう。 これをどのように実装できるかを見ていきます。
enum ManagerType { MT_GTK, MT_UNKNOWN };
プロトタイプテンプレート
このテンプレートを使用すると、既存のオブジェクトを複製することにより、複雑なまたは「重い」オブジェクトを作成できます。 多くの場合、このテンプレートは、複製されたオブジェクトが保存するシングルトンテンプレートと組み合わせて使用されます。 例を考えてみましょう:
ここには、作成する必要のある複雑で重いクラスのComplexObjectがあります。 このクラスを作成するには、シングルトンから取得したProtoComplexObjectオブジェクトをコピーします。
#define BIND_TO_PROTOTYPE(D_iface, D_prototype) \ PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); }
これで、プロトタイプを次のように使用できます。
An<ComplexObject> o; std::cout << o->name << std::endl;
マルチトンテンプレート
たとえば、必要な情報を取得するために、データセンターへの接続を作成する必要があるとします。 データセンターを過負荷にしないために、各データセンターへの接続を1つだけ使用する必要があります。 データセンターが1つしかない場合は、シングルトンを使用し、毎回それを使用してメッセージ/リクエストを送信します。 ただし、2つの同一のデータセンターがあり、それらの間で負荷のバランスを取る必要があります。 可能であれば、両方のデータセンターを使用してください。 したがって、シングルトンはここでは適していませんが、マルチトンは適しています。これにより、オブジェクトの複数のインスタンスをサポートできます。
実装には、最も単純な接続バランシングアルゴリズムが使用されました。接続を使用するための新しい要求はそれぞれ、次のデータセンターにリダイレクトされます。 これは、この設計パターンの効果を説明するのに十分です。最初に2つの接続が作成され、新しい接続オブジェクトに再利用されます。 プログラムの最後に、それらは自動的に破棄されます。
シングルトン、ファクトリー、およびプロトタイプ
最後の例では、いくつかの生成パターンの相乗効果を検討します。 渡された値に応じて異なるオブジェクトを作成する必要があるとします。 作成されるさまざまなタイプの数は非常に多いと想定されているため、目的のタイプを選択するためにかなり迅速な方法を使用したいと思います。 ハッシュ関数を使用した検索を使用します。 目的のタイプの各インスタンスは非常に重いため、インスタンスの作成を容易にするために「プロトタイプ」テンプレートを使用する必要があります。 各プロトタイプを遅延的に生成したい、つまり 必要になるまでプロトタイプを作成しないでください。 また、彼らがこの機能を決して使用しない可能性もあります。したがって、オブジェクトを生成するために事前にオブジェクトを作成する必要はありません。 「遅延」ファクトリーを作成します。
それでは始めましょう。 最初に、作成するインターフェイスとオブジェクトを作成します。
struct IShape : IObject { virtual std::string getShapeName() = 0; virtual int getLeftBoundary() = 0; }; struct Square : IShape { Square() { std::cout << "Square ctor" << std::endl; } Square(const Square& s) { std::cout << "Square copy ctor" << std::endl; } virtual std::string getShapeName() { return "Square"; } virtual int getLeftBoundary() { return m_x; } private:
すべてを「成長した」ように見えるようにする必要のない機能をクラスに追加しました。 クイック検索のために、unordered_mapを使用します。これは、コンパイラが新しい標準をサポートしている場合、boostまたはstdのいずれかにあります。 キーは型を示す文字列で、値は特定の型の必要なインスタンスを生成するオブジェクトになります。 これを行うには、適切なインターフェイスを作成します。
なぜなら 重い施設を作る予定で、工場ではAnClonerを使用します。
struct ShapeFactory { ShapeFactory() { std::cout << "ShareFactory ctor" << std::endl;
これで、工場の準備が整いました。では、息を吸い込んで、オブジェクトを生成する最後の関数を追加しましょう。 void anProduce(An<IShape>& a, const std::string& type) { An<ShapeFactory> factory; factory->produce(a, type); }
これで、ファクトリを使用できます。 std::cout << "Begin" << std::endl; An<IShape> shape; shape.produce("Square"); std::cout << "Name: " << shape->getShapeName() << std::endl; shape.produce("Circle"); std::cout << "Name: " << shape->getShapeName() << std::endl; shape.produce("Square"); std::cout << "Name: " << shape->getShapeName() << std::endl; shape.produce("Parallelogram"); std::cout << "Name: " << shape->getShapeName() << std::endl;
画面に出力されるもの: Begin ShareFactory ctor Square ctor Square copy ctor Name: Square Circle ctor Circle copy ctor Name: Circle Square copy ctor Name: Square Cannot clone the object for unknown type
私たちに何が起こっているのか、さらに詳しく考えてみましょう。 Beginは最初に表示されます。これは、何が起こっているのかという「怠iness」を語るファクトリーやプロトタイプなど、オブジェクトがまだ作成されていないことを意味します。次に、shape.produce( "Square")の呼び出しにより、一連のアクション全体が生成されます。ファクトリー(ShareFactory ctor)が作成され、プロトタイプSquare(Square ctor)が生成され、プロトタイプがコピーされ(Square copy ctor)、目的のオブジェクトが返されます。 getShapeName()メソッドを呼び出し、文字列Square(名前:Square)を返します。 Circleオブジェクトでも同様のプロセスが発生しますが、現在はファクトリがすでに作成されており、再作成と初期化は不要です。次にshape.produce(「Square」)を使用してSquareを作成するとき、プロトタイプのコピーのみが呼び出されるようになりました。プロトタイプ自体はすでに作成されています(Squareコピークター)。不明な図形shape.produce( "Parallelogram")を作成しようとすると、簡潔さのために省略されたハンドラーでキャッチされる例外がスローされます(不明なタイプのオブジェクトを複製できません)。結論
この記事では、生成的なデザインパターンと、さまざまな状況でのそれらの使用について説明します。この記事は、そのようなパターンで完全であると主張していません。ここでは、設計と実装の段階で発生する既知の問題とタスクについて、少し異なる見解を示したいと思いました。このアプローチでは、この記事で説明されているすべての基礎となる非常に重要な原則、依存関係処理の原則[7]を使用します。わかりやすく理解するために、1つのテーブルにさまざまなテンプレートを使用しています。比較表:無条件のインスタンス作成模様 | 通常の使用 | 記事で使用 |
---|
シングルトン | T::getInstance() | An<T> -> |
橋 | T::createInstance() | An<T> -> |
工場 | T::getInstance().create() | An<T> -> |
マルチトン | T::getInstance(instanceId) | An<T> -> |
比較表:入力に基づいたインスタンスの作成模様 | 通常の使用 | 記事で使用 |
---|
工場 | T::getInstance().create(...) | An<T>.produce(...) |
抽象工場 | U::getManager().createT(...) | An<T>.produce(...) |
試作機 | T::getInstance().clone() | An<T>.produce(...) |
シングルトン、プロトタイプおよび工場 | T::getInstance().getPrototype(...).clone() | An<T>.produce(...) |
利点は明らかです。実装はインターフェースを貫通しません。このアプローチにより、コードを特定のインスタンス作成メソッドから抽象化し、解決する問題に集中することができます。これにより、非常に柔軟なアプリケーションを作成でき、適切なコードをリファクタリングする必要なく実装を簡単に変更できます。次は?
そして、参照のリスト。さて、次の記事では、マルチスレッドの問題やその他の興味深い珍しい「バン」について検討します。文学
[1] Habrahabr:シングルトンパターンの使用[2] Habrahabr:シングルトンとオブジェクトの寿命[3] ウィキペディア:ファクトリメソッド[4] ウィキペディア:デザインパターンの生成[5] Andrey on .NET:パターンの生成[6] Andrey on .NET :ファクトリメソッド[7] ウィキペディア:依存関係の反転の原理[8] ウィキペディア:制御の反転