C ++での依存性注入

MagicClass::getInstance().getFooFactory().createFoo().killMePlease();
      
      





「依存性の注入」と「静的型付け」は、常に親友とは言えません。 この方向のいくつかの開発が存在し、簡単にグーグル化されていますが、ハックベース、巧妙なトリック、外部ライブラリの接続の独自のシンプルな実装を作成することがいかに現実的であるかは興味深いです。 柔軟性があまりない場合、文字通り2つのアクション(セットアップと実装)があります。 マルチスレッドの問題は、主旨から逸脱しないように対処されません。 だから、私は個人的に依存性注入から何を望んでいますか。



ステージング


オブジェクトの時間管理 。 寿命に関するニュアンスは、アプリケーションのメインロジックを薄めるべきではありません。 たいていの場合、必要なオブジェクトが単一のクラスのインスタンスであるか、何らかのファクトリーメソッドを使用して作成されていることを知る必要はありません。 使用可能なインスタンスが必要です。 メインアプリケーションロジックに影響を与えることなく、オブジェクトのライフタイムを制御するためのルールを変更できると非常に便利です。



テスト 依存性注入の主要な目標の1つ。 また、メインの実装に影響を与えることなく、特定のオブジェクトをテストスタブに置き換えることができることを意味します。



オブジェクトへの単純化されたアクセス 。 どのオブジェクトから取得できるかについての情報は、ほとんどの場合、まったく興味がありません。 さらに、基本的な機能から注意をそらし、プロジェクトサブシステムのリンクを必要以上に強化します。 また、追加されたサービスに対して適切なアクセスポイントを考えたくないプログラマーは、システムアーキテクチャ全体に悪影響を及ぼす可能性があります。 「最近、モジュール番号Nから必要なオブジェクトをほぼすべて受け取りました。これらもドロップします...」



テスト例付きの作業バージョンは、 ここから取得できます



ビジネスに取り掛かろう。 人生の時間から始めましょう。 次のオプションを使用できるようにしたいと思います。



  1. シングルトン アプリケーションの存続期間中、オブジェクトの静的インスタンスは1つだけです。
  2. パブリックオブジェクト( 共有 )。 一匹狼のように見えます。 主な違いは、誰かが使用している間にオブジェクトが存在することです。 すべてのクライアントが同じインスタンスへのリンクを使用します。 興味のある人がいなければ、作業中に一度だけではなく、何度も作成および破棄できます。
  3. 対象 オブジェクトの寿命は、クライアントの寿命と一致します。
  4. ランタイムオブジェクト すべてのクライアントは同じオブジェクトを使用しますが、プログラムの操作中に変更される可能性があります。




当然、クライアントは埋め込みオブジェクトがどのタイプに属しているかを気にするべきではありません。

2番目の願いは、実装の自然さです。 クラスフィールドの宣言にできるだけ近い構文を使用して、オブジェクトをクライアントに埋め込みたいです。 実際、これはクラスプロパティの一種です。



 class SomeClass { public: private: inject(SomeInterface, mFieldName) };
      
      







もちろん、当初、私の夢にはテンプレートがあったことをすぐに認めなければなりませんが、どういうわけか上記の実装タイプのシンプルで透過的な実装を取得できませんでした。 しかし、ありふれたマクロでは、予想外に簡単になりました。



プロジェクトには、依存関係が次のように構成されている場所が必要です。



 inject_as_share(Interface1, Class1) inject_as_singleton(Interface2, Class1) inject_as_object(Interface3, Class1) inject_as_runtime(Interface4)
      
      







最初のパラメーターはインターフェースで、2番目はそれを実装するクラスです。 もちろん、クラスはそれ自体のインターフェースとして機能できます。 ランタイムオブジェクトの場合、プログラムの深さのどこかで初期化も必要になります(最も成功した実装オプションではありませんが、必要な場合もあります)。



 inject_set_runtime(Interface4, &implementation4)
      
      







最初に目を引くのは、 inject_as_shareinject_as_singletonおよびinject_as_objectがクラスのインスタンスを作成できることです。これは、コンストラクターを呼び出すことを意味します。 単純さは単純ですが、デフォルトのコンストラクターのみに依存するのはあまりにも簡単です。 したがって、対応するクラスのコンストラクターのパラメーターを初期化マクロに渡すことができます。



 inject_as_share(Interface1, Class1, "param1", 1234, true)
      
      







提案された概念の実装を掘り下げる前に、オブジェクトo1のo2への依存関係を設定する他の方法について簡単に説明したいと思います。



  1. o1は、o2インスタンスを直接作成します。 o2を置き換えるための最も「前頭的」で柔軟性のない方法は、ソースをシャベルでシャベルし、作成を別のものに置き換える必要があります。 o1がo2を作成したため、通常、破壊の責任を負うのは彼です。
  2. o2をパラメーターとしてコンストラクターに渡します。 依存関係を実装するためのかなり一般的な方法ですが、クライアントクラスに多くの依存関係やコンストラクタがある場合は不便かもしれません。
  3. ファクトリメソッド/クラス。 このアプローチにより、o2オブジェクトの作成の複雑さを効果的にカプセル化できますが、工場自体(特に多くの場合)は、アプリケーションアーキテクチャに情報ノイズを追加します。 ファクトリのもう1つの重大な欠点は、その使用がクライアントクラスインターフェイスに反映されないことです。


コンフィギュレーター


実装に移りましょう。 lonersの導入から始めましょう。 ユーザーの場合、設定全体が行に縮小されます



 inject_as_singleton(Interface, Class, [constructor_parameters_list])
      
      







コンストラクター引数の数は一般に不明であるため、 inject_as_singletonは可変数の引数を持つマクロです。 すべてのタイプのインジェクションは(クラスのプロパティとして)一様に使用されるため、最初に、オブジェクトの作成方法に関係なく、オブジェクトへのアクセスを許可するインターフェイスを考え出す必要があります。 この場合、指定されたインターフェイスを実装するオブジェクトへの参照を返す単一のgetメソッドを持つFactory構造になります。



 struct Factory { Interface& get(); };
      
      







単一クラスの実装はクラシックになります



 Interface& getInstance() { static T instance = T(...); return instance; }
      
      







コンストラクタの呼び出しは、少々珍しい(少なくとも私にとっては)形式で記述されなければなりませんでした。



 static T instance(...);
      
      





パラメーターのないコンストラクターの場合、コンパイラーはそれを関数宣言として頑固に解釈します。



また、競合を避けるために、この実装はすべて、injectorXXX名前空間に配置します。XXXは、実装インターフェイスの名前です。 結果のマクロは次のようになります



 #define inject_as_singleton(Interface, T, ...) \ namespace injector##Interface{ \ Interface& getInstance() \ { \ static T instance = T(__VA_ARGS__); \ return instance; \ } \ struct Factory \ { \ Interface& get() { return getInstance(); } \ }; \ }
      
      







これ以上詳しく説明しませんが、アプローチは常に同じです。ライフタイムを管理するための「仕組み」戦略の実装+固定されたFactoryインターフェース構造、およびこれらはすべて別の名前空間にあります。



公共施設に移りましょう。 誰かがそれを必要とする間、それは存在しますが、孤独とは異なり、それは破壊され、再作成され、まったく作成されません。 参照カウントを使用してこの戦略を実装します-実装されたクラスの工場所有者、つまりユーザーの数。



 #define inject_as_share(Interface, T, ...) \ namespace injector##Interface{ \ struct Factory \ { \ Factory() { \ if (refCount == 0) { \ object = new T(__VA_ARGS__); \ } \ ++refCount; \ } \ ~Factory() { \ if (--refCount == 0) { \ delete object; \ } \ } \ Interface& get() { return *object; } \ static T* object; \ static unsigned int refCount; \ }; \ T* Factory::object = 0; \ unsigned int Factory::refCount = 0; \ }
      
      







コードには特別な説明は必要ありません-コンストラクターでカウンターを増やし、必要に応じて実装されたクラスのインスタンスを作成し、デストラクターでカウンターを減らし、時間が経過したら不要なオブジェクトを削除します。



「各クライアントの独自オブジェクト」の実装は、ファクトリフィールドとして非常に簡単に実装されます。



 #define inject_as_object(Interface, T, ...) \ namespace injector##Interface{ \ struct Factory \ { \ Factory() : object(__VA_ARGS__) {} \ Interface& get() { return object; } \ T object; \ }; \ }
      
      







ファクトリーは、プログラムの実行中に変更できるオブジェクトを所有せず、単に最後に指定されたインスタンスを提供し、それを変更できるようにします。



 #define inject_as_runtime(Interface) \ namespace injector##Interface{ \ struct Factory \ { \ Interface& get() { return *object; } \ static Interface* object; \ }; \ Interface* Factory::object = 0; \ }
      
      







戦略の最終的な実装には、オブジェクトを設定するための特別なマクロが必要になります



 #define inject_set_runtime(Interface, Value) injector##Interface::Factory::object = (Value);
      
      





実装


工場が完成したら、実装そのものに移りましょう。 戦略は次のようになります-オブジェクトは対象のファクトリーのインスタンスを所有し、必要に応じて提供するオブジェクトを参照します。 もちろん、ここでの「工場」という用語は不完全です。なぜなら、それが常にコピーを作成するわけではないからです。しかし、執筆時点では、これ以上成功するものはないと考えられていました。



ここで問題に直面しています。 毎回ユーザーに手動でgetメソッドを強制的に呼び出させたくありません。結局のところ、メカニズムのさらなる改善のために手を放すために、ファクトリーの存在の事実を隠したいと思います。 どういうわけか、通常のプロパティのシミュレーターを作成する必要があります。 最初に頭に浮かぶのは、継承によるインターフェイスの実装です。 それが来ると、それは去ります。 これが実現可能な場合、それは非常に重要です。 したがって、より簡単に行います-演算子を再定義するラッパークラスを作成します->実装されたクラスのインスタンスを強制的に返します。 それはほぼ完璧になります。 制限の中で、ユーザーは埋め込みオブジェクトをポインターとして使用する義務のみに直面しますが、これはすでにかなり許容できます。



したがって、プロパティ埋め込みマクロは次のようになります

 #define inject(Interface, Name) \ struct Interface##Proxy \ { \ Interface* operator->() \ { \ return &factory.get(); \ } \ injector##Interface::Factory factory; \ }; \ Interface##Proxy Name;
      
      







目的のファクトリを所有し、演算子でアクセスするラッパークラスのみ->





ご覧のとおり、実装は非常に簡単です。 提案されたメカニズムの使用はさらに簡単です。 簡単な例を考えてみましょう。 インターフェースを作ろう



 class IWork { public: virtual void doSmth() = 0; virtual ~IWork() {}; };
      
      







そして、いくつかのクラスWork1、Work2など、それを実装します。これらのクラスのいくつかには、パラメーターを持つコンストラクターがあります(例の全文はこちら )。 まず、構成する



 inject_as_share(IWork, Work1) inject_as_singleton(Work2, Work2, 1) inject_as_object(Work3, Work3, 1, true) inject_as_runtime(Work4)
      
      







次に、実験用のクラスを作成します



 class Employee { public: void doIt() { mWork1->doSmth(); mWork2->doSmth(); mWork3->doSmth(); mWork4->doSmth(); } private: inject(IWork, mWork1) inject(Work2, mWork2) inject(Work3, mWork3) inject(Work4, mWork4) };
      
      







そして使用する



 Work4 w4; inject_set_runtime(Work4, &w4) Employee e1; e1.doIt();
      
      







ここで最も重要なことは、 Employeeクライアントクラスが埋め込みオブジェクトの存続期間についてまったく仮定しないことです。inject_as_XXX構成ディレクティブがこれを処理します。 完全な例では、クライアントオブジェクトを作成および破棄するときに、埋め込みオブジェクトがどのように感じるかを詳細に観察できます。



以上です。 この方法の欠点の中で、潜在的に十分に大きく関連性の高い構成ファイルが必要であることに注意したいと思います。これには、ほぼすべてのプロジェクトソースを含める必要があります。 ただし、これらのファイルは完全に線形であり、構成の内訳を事前に考えることを禁止するものはありません。 ただし、プロジェクトの他の部分では、接続性のレベルが大幅に低下するため、開発者は実装されたロジックに集中できます。



All Articles