.NETにDIを実装する「正しい」方法は何ですか?


22

比較的大きなアプリケーションに依存性注入を実装したいと考えていますが、経験はありません。私は、UnityやNinjectなどのIoCの概念といくつかの実装、および利用可能な依存関係インジェクターを研究しました。しかし、私を避けているものが1つあります。アプリケーションでインスタンス作成を整理するにはどうすればよいですか?

私が考えているのは、いくつかの特定のクラスタイプのオブジェクトを作成するロジックを含むいくつかの特定のファクトリを作成できるということです。基本的に、このクラスの静的カーネルインスタンスのNinject Get()メソッドを呼び出すメソッドを持つ静的クラス。

アプリケーションに依存性注入を実装する正しいアプローチでしょうか、それとも他の原則に従って実装する必要がありますか?


5
正しい方法があるとは思いませんがプロジェクトに応じて多くの正しい方法があります。すべての依存関係が1つのポイントに注入されることを確認できるので、他のものに固執し、コンストラクター注入を提案します。さらに、コンストラクターのシグネチャが長くなりすぎると、クラスの処理が多すぎることがわかります。
ポールケルシャー

どんな種類の.netプロジェクトを構築しているか知らずに答えることは困難です。WPFの良い答えは、たとえばMVCの悪い答えかもしれません。
JMK

ソリューションまたは各プロジェクトのDIモジュールにすべての依存関係の登録を整理し、場合によってはテストする詳細に応じていくつかのテストに1つを整理すると便利です。ああ、もちろん、コンストラクターインジェクションを使用する必要があります。他のものは、より高度でクレイジーな使用法です。
マークロジャース

回答:


30

使用するツールについてはまだ考えないでください。IoCコンテナなしでDIを実行できます。

最初のポイント:Mark Seemannには、.NetのDIに関する非常に良い本があります

2番目:コンポジションルート。プロジェクト全体のエントリポイントで設定が完了していることを確認してください。残りのコードは、使用されているツールについてではなく、注入について知っている必要があります。

3番目:コンストラクターインジェクションは、最も可能性の高い方法です(必要ない場合もありますが、それほど多くはありません)。

4番目:ラムダファクトリと他の同様の機能を使用して、インジェクションのみの目的で不要なインターフェイス/クラスを作成しないようにします。


5
すべての優れたアドバイス。特に最初の部分:純粋なDIの実行方法を学び、そのアプローチで必要な定型コードの量を減らす可能性のあるIoCコンテナーを調べ始めます。
デビッドアルノ

6
...または、IoCコンテナをすべてスキップして、静的検証のすべての利点を維持します。
デン

私は、DIに入る前にこのアドバイスが良い出発点であるのが好きで、実際にMark Seemannの本を持っています。
スヌープ

この答えを二度目にすることができます。ブートストラップロジックの一部をアプリケーションを構成するモジュールに分解することで、非常に大きなアプリケーションでpoor-mans-DI(手書きブートストラップ)を使用することに成功しました。
不安定な

1
注意してください。ラムダ注入を使用することは、特にテストによって引き起こされる設計損傷の種類について、狂気への迅速な道筋になる可能性があります。知っている。私はその道を進んだ。
jpmc26

13

質問には2つの部分があります。DIを適切に実装する方法と、DIを使用するために大規模なアプリケーションをリファクタリングする方法です。

最初の部分は@Miyamoto Akiraによってよく回答されています(特に、Mark Seemannの「.netでの依存性注入」の本を読むことをお勧めします。Marks ブログも無料の良いリソースです。

2番目の部分はかなり複雑です。

良い最初のステップは、すべてのインスタンス化を単純にクラスコンストラクターに移動することですnew。依存関係を注入せず、コンストラクターでのみ呼び出すようにしてください。

これにより、これまでに行ったすべてのSRP違反が強調表示されるため、クラスをより小さな協力者に分類し始めることができます。

次の問題は、構築時に実行時パラメーターに依存するクラスです。通常、これを修正するには、単純なファクトリを作成Func<param,type>します。多くの場合、でコンストラクタを初期化し、メソッドで呼び出します。

次の手順では、依存関係のインターフェイスを作成し、これらのインターフェイスを除くクラスに2つ目のコンストラクタを追加します。パラメーターなしのコンストラクターは、具象インスタンスを更新し、それらを新しいコンストラクターに渡します。これは一般に「B * stard Injection」または「Poor mans DI」と呼ばれます。

これにより、ユニットテストを実行できるようになります。それがリファクタリングの主な目標である場合は、停止することができます。新しいコードはコンストラクターインジェクションで記述されますが、古いコードは記述されたまま動作し続けますが、それでもテスト可能です。

もちろん、さらに先へ進むことができます。IOCコンテナーを使用する場合、次のステップはnew、パラメーターレスコンストラクターへのすべての直接呼び出しをIOCコンテナーへの静的呼び出しに置き換え、基本的に(ab)サービスロケーターとして使用することです。

これにより、以前と同様に処理するランタイムコンストラクターパラメーターのケースが多くなります。

これが完了すると、パラメータのないコンストラクターの削除を開始し、純粋なDIにリファクタリングできます。

最終的にこれは多くの作業になるので、なぜそれをしたいのかを決定し、リファクタリングから最も恩恵を受けるコードベースの部分に優先順位を付けてください


3
非常に精巧な答えをありがとう。あなたは私が直面している問題にどのようにアプローチするかについていくつかのアイデアをくれました。アプリのアーキテクチャ全体は、すでにIoCを念頭に置いて構築されています。DIを使用したい主な理由はユニットテストでさえありません。ボーナスとしてではなく、アプリケーションのコアで定義されたさまざまなインターフェイスのさまざまな実装をできるだけ少ない労力で交換する機能です。問題のアプリケーションは絶えず変化する環境で動作し、環境の変化に応じて新しい実装を使用するためにアプリケーションの一部を頻繁に交換する必要があります。
user3223738

1
喜んで助けてくれました。DIの最大の利点は疎結合であり、これがもたらす再構成の容易さであり、ユニットテストは素晴らしい副作用です。
スティーブ

1

最初に、新しいプロジェクトを開始するのではなく、既存のプロジェクトをリファクタリングすることにより、これを自分自身で非常に難しくしていることに言及したいと思います。

あなたはそれが大きなアプリケーションだと言ったので、最初から小さなコンポーネントを選んでください。できれば、「リーフノード」コンポーネントで、他のコンポーネントによって使用されていないものを使用してください。このアプリケーションの自動テストの状態はわかりませんが、このコンポーネントのすべての単体テストに違反することになります。そのために準備してください。ステップ0は、まだ存在しない場合に変更するコンポーネントの統合テストを作成しています。最後の手段として(テストインフラストラクチャなし、それを記述するための賛同なし)、このコンポーネントが機能していることを確認するために実行できる一連の手動テストを見つけてください。

DIリファクタリングの目標を述べる最も簡単な方法は、このコンポーネントから「new」演算子のすべてのインスタンスを削除することです。これらは一般に2つのカテゴリに分類されます。

  1. 不変メンバー変数:これらは一度(通常はコンストラクターで)設定される変数であり、オブジェクトの存続期間中は再割り当てされません。これらの場合、オブジェクトのインスタンスをコンストラクターに注入できます。通常、これらのオブジェクトを破棄する責任はありません(ここでは絶対に言いたくありませんが、実際にその責任を負うべきではありません)。

  2. バリアントメンバー変数/メソッド変数:これらは、オブジェクトの存続期間中のある時点でガベージコレクションが行われる変数です。これらのために、これらのインスタンスを提供するためにクラスにファクトリーを注入する必要があります。ファクトリーによって作成されたオブジェクトを破棄する責任があります。

IoCコンテナー(その名のとおり)は、これらのオブジェクトをインスタンス化し、ファクトリーインターフェースを実装する責任を負います。変更したコンポーネントを使用しているものは何でも、コンポーネントを取得できるようにIoCコンテナについて知る必要があります。

上記を完了すると、選択したコンポーネントのDIから得たいと思っているメリットを享受できます。これらの単体テストを追加/修正する良い機会になります。既存の単体テストがあった場合、実際のオブジェクトを注入してパッチを適用するか、モックを使用して新しい単体テストを作成するかを決定する必要があります。

アプリケーションの各コンポーネントに対して上記を「単純に」繰り返し、メインのみがそれを知る必要があるまで、IoCコンテナーへの参照を上に移動します。


1
良いアドバイス:「大きな書き直し」ではなく、小さなコンポーネントから始めてください
ダニエル・ホリンレイク

0

正しいアプローチは、使用する場合、コンストラクター注入を使用することです

私が考えているのは、いくつかの特定のクラスタイプのオブジェクトを作成するロジックを含むいくつかの特定のファクトリを作成できるということです。基本的に、このクラスの静的カーネルインスタンスのNinject Get()メソッドを呼び出すメソッドを持つ静的クラス。

依存性注入よりも、サービスロケーターになります。


はい。コンストラクター注入。引数の1つとしてインターフェース実装を受け入れるクラスがあるとしましょう。ただし、インターフェイス実装のインスタンスを作成して、このコンストラクターのどこかに渡す必要があります。できれば、集中化されたコードにする必要があります。
user3223738

2
いいえ、DIコンテナーを初期化するときに、インターフェイスの実装を指定する必要があります。DIコンテナーはインスタンスを作成し、コンストラクターに注入します。
低空飛ぶペリカン

個人的には、コンストラクターインジェクションは使いすぎになっています。私はあまりにも頻繁に10種類のサービスが注入され、関数呼び出しに実際に必要なのは1つだけであることを見てきました-なぜその関数の引数の一部ではないのですか?
urbanhusky

2
10個の異なるサービスが注入されるのは、誰かがSRPに違反しているためです。SRPは小さなコンポーネントに分割する必要があります。
低空飛ぶペリカン

1
@Fabio質問はそれがあなたを何を買うかです。まったく異なるものを扱う巨大なクラスを作成するのが良いデザインである例をまだ見ていません。DIが行う唯一のことは、これらの違反をすべて明らかにすることです。
Voo

0

あなたはそれを使いたいと言いますが、その理由は述べません。

DIは、インターフェイスから結石を生成するメカニズムを提供する以外の何物でもありません。

これ自体はDIPに由来します。コードが既にこのスタイルで記述されており、結石が生成される場所が1つしかない場合、DIはそれ以上何もパーティーにもたらしません。ここにDIフレームワークコードを追加すると、コードベースが単純に大きくなり難読化されます。

使用たいと仮定すると、通常、アプリケーション/アプリケーションの初期段階でファクトリー/ビルダー/コンテナー(またはその他)をセットアップして、明確に見えるようにします。

注意:Ninject / StructureMapなどにコミットするのではなく、希望する場合は簡単にロールバックできます。しかし、スタッフの適度な離職がある場合は、認知されたフレームワークを使用するために車輪に油を塗るか、少なくともそのスタイルでそれを書いて、学習曲線になりすぎないようにすることができます。


0

実際、「正しい」方法は、他の選択肢がまったくない場合を除き、工場をまったく使用しないことです(単体テストや特定のモックのように-量産コードの場合は工場を使用しません)。そうすることは、実際にはアンチパターンであり、どんな場合でも避けるべきです。DIコンテナーの背後にある全体的なポイントは、ガジェットが作業行えるようにすることです。

前の投稿で前述したように、IoCガジェットにアプリ内のさまざまな依存オブジェクトの作成を担当させる必要があります。つまり、DIガジェットにさまざまなインスタンス自体を作成および管理させます。これがDIの背後にある全体のポイントです。オブジェクトは、依存するオブジェクトを作成および/または管理する方法を決して知ってはいけません。そうしないと疎結合が壊れます。

既存のアプリケーションをすべてのDIに変換することは大きなステップですが、そうする際の明らかな困難は別として、あなたのバインディングの大部分を自動的に実行するDIツールを探索することも必要になります。 (Ninjectのようなものの中核は、"kernel.Bind<someInterface>().To<someConcreteClass>()"インターフェイス宣言をそれらのインターフェイスを実装するために使用したい具体的なクラスに一致させるために行う呼び出しです。DI ガジェットがコンストラクター呼び出しをインターセプトして提供できるようにする「バインド」呼び出しですいくつかのクラスの典型的なコンストラクター(ここに示す擬似コード)は次のとおりです。

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

そのコードのどこにも、SomeConcreteClassAまたはSomeOtherConcreteClassBのインスタンスを作成/管理/リリースするコードがなかったことに注意してください。実際のところ、どちらの具体的なクラスも参照されていません。それで...魔法はどこで起こったのですか?

アプリの起動部分では、次の処理が行われました(これも擬似コードですが、実際の(Ninject)にかなり近い...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

そこにある少しのコードは、Ninjectガジェットにコンストラクターを探し、それらをスキャンし、処理するように構成されたインターフェースのインスタンスを探して(「バインド」呼び出し)、どこでも具象クラスのインスタンスを作成して置換するように指示しますインスタンスが参照されます。

Ninjectを補完する優れたツールがあり、Ninject.Extensions.Conventions(別のNuGetパッケージ)と呼ばれ、この作業の大部分を実行します。これを自分で構築する際に経験する優れた学習体験から離れることはありませんが、自分で始めるには、これは調査するためのツールかもしれません。

メモリが機能する場合、Unity(以前はMicrosoftから現在はオープンソースプロジェクト)には、同じことを行うメソッド呼び出しが2つあり、他のツールには同様のヘルパーがあります。

DIトレー​​ニングの大部分については、Mark Seemannの本を読んでくださいNinjectの本で、Ninject専用に書かれた別のリソースを紹介します。私はそれと良い読み物を持っています:Dependency InjectionのためのNinjectの習得


0

「正しい方法」はありませんが、従うべき簡単な原則がいくつかあります。

  • アプリケーションの起動時にコンポジションルートを作成する
  • コンポジションルートが作成された後、DIコンテナ/カーネルへの参照を破棄します(または、少なくともそれをカプセル化して、アプリケーションから直接アクセスできないようにします)
  • 「新規」経由でインスタンスを作成しないでください
  • 必要なすべての依存関係を抽象化としてコンストラクターに渡します

それで全部です。確かに、それは法律ではなく原則ですが、あなたがそれらに従うなら、あなたはDIをすることを確信できます(私が間違っているなら私を修正してください)。


では、「新規」なしでDIコンテナを知らずに、実行時にオブジェクトを作成する方法は?

NInjectの場合、ファクトリーの作成を提供するファクトリー拡張があります。もちろん、作成されたファクトリーにはカーネルへの内部参照がまだありますが、アプリケーションからはアクセスできません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.