依存関係注入(C#)をいつ使用するべきか[終了]


8

依存性注入(DI)の概念を確実に理解したいと思います。まあ、私は実際にコンセプトを理解しています。DIは複雑ではありません。インターフェイスを作成し、それを使用するクラスにインターフェイスの実装を渡します。これを渡す一般的な方法はコンストラクターですが、セッターやその他のメソッドで渡すこともできます。

DIをいつ使用するかがよくわかりません。

使用法1:もちろん、インターフェースの実装が複数ある場合にDIを使用するのは論理的であるようです。SQL Serverのリポジトリがあり、次にOracleデータベースのリポジトリがあります。どちらも同じインターフェースを共有し、実行時に必要なインターフェースを「挿入」します(これが使用される用語です)。これはDIではありません。ここでは基本的なOOプログラミングです。

使用法2:特定のメソッドをすべて持つ多くのサービスを持つビジネスレイヤーがある場合、各サービスのインターフェイスを作成し、これが一意であっても実装を注入することをお勧めします。これはメンテナンスに適しているからです。これは私が理解できないこの2番目の使用法です。

私は50のビジネスクラスのようなものを持っています。それらの間で共通するものはありません。いくつかは、3つの異なるデータベースでデータを取得または保存するリポジトリです。一部のファイルの読み取りまたは書き込み。一部は純粋なビジネスアクションを行います。特定のバリデーターとヘルパーもあります。一部のクラスは異なる場所からインスタンス化されるため、課題はメモリ管理です。バリデーターは、いくつかのリポジトリーや、同じリポジトリーを再度呼び出すことができる他のバリデーターを呼び出すことができます。

例:ビジネスレイヤー

public class SiteService : Service, ICrud<Site>
{
    public Site Read(Item item, Site site)
    {
        return beper4DbContext.Site
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public Site Read(string itemCode, string siteCode)
    {       
        using (var itemService = new ItemService())
        {
            var item = itemService.Read(itemCode);
            return Read(item, site);
        }
    }
}
public class ItemSiteService : Service, ICrud<Site>
{
    public ItemSite Read(Item item, Site site)
    {
        return beper4DbContext.ItemSite
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public ItemSite Read(string itemCode, string siteCode)
    {        
        using (var itemService = new ItemService())
        using (var siteService = new SiteService())
        {
            var item = itemService.Read(itemCode);
            var site = siteService.Read(itemCode, siteCode);
            return Read(item, site);
        }
    }
}

コントローラ

public class ItemSiteController : BaseController
{
    [Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
    public IHttpActionResult Get(string itemCode, string siteCode)
    {
        using (var service = new ItemSiteService())
        {
            var itemSite = service.Read(itemCode, siteCode);
            return Ok(itemSite);
        }
    }
}

この例は非常に基本的ですが、itemServiceの2つのインスタンスを簡単に作成してitemSiteを取得する方法がわかります。次に、各サービスにはDBコンテキストが付属しています。したがって、この呼び出しは3つのDbContextを作成します。3接続。

私の最初のアイデアは、以下のようにこのすべてのコードを書き直すシングルトンを作成することでした。コードはより読みやすく、最も重要なのは、シングルトンシステムが使用する各サービスのインスタンスを1つだけ作成し、最初の呼び出しでそれを作成することです。パーフェクトです。ただし、異なるコンテキストがありますが、コンテキストに同じシステムを使用できます。できました。

ビジネス層

public class SiteService : Service, ICrud<Site>
{
    public Site Read(Item item, Site site)
    {
        return beper4DbContext.Site
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public Site Read(string itemCode, string siteCode)
    {       
            var item = ItemService.Instance.Read(itemCode);
            return Read(item, site);
    }
}
public class ItemSiteService : Service, ICrud<Site>
{
    public ItemSite Read(Item item, Site site)
    {
        return beper4DbContext.ItemSite
            .AsNoTracking()
            .SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
    }

    public ItemSite Read(string itemCode, string siteCode)
    {        
            var item = ItemService.Instance.Read(itemCode);
            var site = SiteService.Instance.Read(itemCode, siteCode);
            return Read(item, site);       
    }
}

コントローラ

public class ItemSiteController : BaseController
{
    [Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
    public IHttpActionResult Get(string itemCode, string siteCode)
    {
            var itemSite = service.Instance.Read(itemCode, siteCode);
            return Ok(itemSite);
    }
}

一部の人々は私が単一のインスタンスでDIを使用する必要がある良い習慣に従って私に言って、シングルトンの使用は悪い習慣です。各ビジネスクラスのインターフェイスを作成し、DIコンテナーを使用してインスタンス化する必要があります。本当に?このDIは私のコードを簡素化します。信じがたい。


4
DIは読みやすさを重視したことがありません。
Robert Harvey

回答:


14

DIの最も「人気のある」ユースケース(すでに説明した「戦略」パターンの使用は別として)は、おそらくユニットテストです。

注入されたインターフェースの「実際の」実装は1つだけだと思っていても、単体テストを行う場合、通常は2番目の実装があります。それは、独立したテストを可能にすることだけを目的とした「模擬」実装です。これにより、複雑さ、起こり得るバグ、そしておそらく「実際の」コンポーネントのパフォーマンスへの影響に対処する必要がないという利点が得られます。

いいえ、DIは可読性を高めるためのものではなく、テスト性を高めるために使用されます(もちろん、排他的ではありません)。

これはそれ自体が目的ではありません。クラスItemServiceが非常に単純なクラスであり、外部ネットワークやデータベースへのアクセスを行わないため、のような単体テストの作成を妨げないSiteService場合、後者を個別にテストすることは努力に値する可能性があるため、DIは使用できません必要。ただし、ItemServiceネットワークで他のサイトにアクセスしている、あなたはおそらくユニットテストになるでしょうSiteService「本当の」置き換えることにより実現することができ、それから切り離され、ItemServiceことによってはMockItemService、いくつかのハードコーディングされた偽のアイテムを提供しています、。

別のことを指摘しておきます。例では、コアビジネスロジックをテストするためにここでDIは必要ないと主張するかもしれませReadん。 DIなしでテストされたユニット)、およびItemService以前のロジックに接続するための単なる「接着剤」コードです。示されているケースでは、これは確かにDIに対する有効な議論です。実際、テスト性を犠牲にすることなくDIを回避できる場合は、先に進みます。しかし、実際のすべてのコードがそれほど単純であるとは限りません。多くの場合、DIは「十分な」ユニットテストを実現するための最も単純なソリューションです。


1
私は本当にそれを手に入れましたが、もしユニットテストをしなかったらどうでしょう。偽のデータベースとの統合テストを行っています。変更は頻繁に行われるため、単体テストを維持できず、本番データは常に変更されます。したがって、実際のデータのコピーを使用したテストは、真剣にテストできる唯一の方法です。ユニットテストの代わりに、すべてのメソッドをコードコントラクトで保護します。私はこれが同じではないことを知っており、ユニットテストをプラスで知っていますが、ユニットテストを優先する時間はありません。それは選択ではなく、事実です。
バスティアンヴァンダム

1
@BastienVandamme:「ユニットテストをまったく行わない」と「すべてをユニットテストする」の間には大きなスペースがあります。そして、私は、少なくとも一部のユニットテストを使用することでいくつかの利点を見つけることができなかった巨大なプロジェクトを見たことがありません。それらのパーツを特定し、単体テスト可能にするためにDIが必要かどうかを確認します。
Doc Brown、

4

依存性注入を使用しないことにより、他のオブジェクトへの永続的な接続を作成できます。あなたが人々を驚かせる場所の内側に隠すことができる接続。作成しているものを書き換えることによってのみ変更できる接続。

それよりも、依存性注入(または私のような古い学校の場合は参照渡し)を使用して、オブジェクトが必要とする方法を定義することを強制することなく、オブジェクトが明示的に必要とするものを明示することができます。

これは多くのパラメータを受け入れることを強制します。デフォルトが明らかなものでも。C#では、ラッキーSODには名前付き引数とオプションの引数があります。つまり、デフォルトの引数があるということです。デフォルトに静的にバインドされることを気にしない場合は、デフォルトを使用しなくても、オプションに圧倒されることなくDIを許可できます。これは、構成に関する規約に従います

テストはDIの正当な理由にはなりません。誰かがリフレクションやその他の魔法を使用して、以前の作業方法に戻り、残りを魔法を使用できることを納得させるウィズバンモックフレームワークを誰かに売ると思われる瞬間。

テストを正しく使用すると、デザインが分離されているかどうかを確認するのに適しています。しかし、それが目的ではありません。十分な魔法ですべてが分離されていることを営業担当者が証明しようとするのを止めることはありません。魔法を最小限に抑えます。

この分離のポイントは、変更を管理することです。1つの場所で1つの変更を行うことができると便利です。狂気が終わることを期待して、ファイルをたどってファイルをたどる必要があるのはよくありません。

単体テストを拒否する店に私を置いてください、そして私はまだDIをします。必要なものとその方法を分離できるので、私はそれを行います。テストまたはテストなし私はその分離を望みます。


2
テストがDIの正当な理由にならない理由を詳しく説明できますか?あなたはそれを魔法の反射フレームワークと混同しているようです-そして私はそれらを嫌うことを理解しています-しかし、アイデア自体には何の反論もしません。
Jacob Raihle

1
あざけるような議論は滑りやすい斜面のようです。私はモックフレームワークを使用していませんが、DIの利点を理解しています。
Robert Harvey

2
それは単なる警告の尾です。自動テストを有効にすることがDIを行う唯一の理由であると考えるとき、彼らは設計の柔軟性への利点を理解するのを逃すだけでなく、仕事なしで利益を約束する製品の売り込みに対しても脆弱になります。DIは仕事です。無料の昼食はありません。
candied_orange

3
言い換えれば、DIの真の理由はデカップリングです。テストは、デカップリングの利点が明らかな最も一般的な領域の1つにすぎません。
StackOverthrow

2
おい、冗談じゃない。DIがあなたのコードを維持するのに役立たない場合、それを使用しないでください。
candied_orange

2

DIのヘリコプタービューは、単にインターフェイスの実装を交換する機能です。もちろん、これはテストにとって朗報ですが、他にも潜在的な利点があります。

オブジェクトのバージョン管理の実装

メソッドが中間層のインターフェースパラメーターを受け入れる場合、実装をスワップアウトするために作成する必要があるコードの量を削減する、トップレイヤーで任意の実装を自由に渡すことができます。確かに、これはとにかくインターフェースの利点ですが、コードがDIを念頭に置いて書かれている場合は、すぐにこの利点を実現できます。

レイヤーを通過する必要があるオブジェクトの数を減らす

これは主にDIフレームワークに適用されますが、オブジェクトAオブジェクトBのインスタンスを必要とする場合、カーネル(または何でも)にクエリを実行して、オブジェクトBをレイヤーに渡すのではなく、オンザフライで生成できます。これにより、作成およびテストが必要なコードの量が削減されます。また、オブジェクトBを気にしないレイヤーをクリーンに保ちます


2

DIを使用するためにインターフェースを使用する必要はありません。DIの主な目的は、オブジェクトの構築と使用を分離することです。

ほとんどの場合、シングルトンの使用は不快に思われます。理由の1つは、クラスの依存関係の概要を把握することが非常に難しくなることです。

あなたの例では、ItemSiteControllerは単にItemSiteServiceをコンストラクター引数として取ることができます。これにより、オブジェクトを作成するコストを回避できますが、シングルトンの柔軟性がなくなります。同じことがItemSiteServiceにも当てはまります。ItemServiceとSiteServiceが必要な場合は、それらをコンストラクターに挿入します。

すべてのオブジェクトが依存関係の注入を使用する場合、利点は最大です。これにより、構築を専用モジュールに集中化したり、DIコンテナーに委任したりできます。

依存関係の階層は次のようになります。

public interface IStorage
{
}

public class DbStorage : IStorage
{
    public DbStorage(string connectionString){}
}

public class FileStorage : IStorage
{
    public FileStorage(FileInfo file){}
}

public class MemoryStorage : IStorage
{
}

public class CachingStorage : IStorage
{
    public CachingStorage(IStorage storage) { }
}

public class MyService
{
    public MyService(IStorage storage){}
}

public class Controller
{
    public Controller(MyService service){}
}

コンストラクター・パラメーターのないクラスは1つだけであり、インターフェースは1つしかないことに注意してください。DIコンテナーを構成するときに、使用するストレージを決定したり、キャッシュを使用するかどうかを決定したりできます。使用するデータベースや他の種類のストレージを使用するかどうかを決定できるため、テストが簡単です。コンテナーオブジェクトのコンテキスト内で、必要に応じてオブジェクトをシングルトンとして扱うようにDIコンテナーを構成することもできます。


「あなたの例では、ItemSiteControllerは単にItemSiteServiceをコンストラクター引数として取ることができます。」もちろん、コントローラは1〜10のサービスを使用できます。次に、新しいサービスを1つ追加して、コンストラクターの署名を変更するように強制することができます。これはどのようにメンテナンスに優れていると言えますか?また、シングルトンメカニズムがないため、オブジェクトを複数回作成するコストはおそらくかかりません。そのため、それを使用する各コントローラーに渡す必要があります。開発者が常に再作成していないことを確認するにはどうすればよいですか。一度だけ作成できるシステムはどれですか?
バスティアンヴァンダム

ほとんどの人と同じように、DIパターンとコンテナー機能を混在させていると思います。あなたの例は私の使用法1です。私はこれに挑戦しません。インターフェースの実装が複数ある場合は、DIと呼ばれるものを使用する必要があります。私はそれをOOPと呼んでいます。なんでも。ほとんどの人がテストやメンテナンスに使用していると思われるUsage 2に挑戦します。理解しましたが、これは私の単一インスタンス要求を解決しません。うまくいけば、コンテナは解決策を提供するようです。コンテナ…DIだけではありません。単一インスタンス機能を備えたDIが必要だと思います。これはコンテナー(サードパーティライブラリ)にあります。
Bastien Vandamme

@ bastien-vandamme新しいサービスを追加する必要がある場合、それらをコンストラクタに追加する必要がありますが、これは問題にはなりません。多くのサービスを注入する必要がある場合、これは依存関係を注入するという考えではなく、アーキテクチャの問題を示している可能性があります。同僚を適切に設計し、トレーニングすることで、同じインスタンスが他の開発者によって再利用されるようにします。しかし、一般的には、クラスの複数のインスタンスを持つことが可能であるべきです。例として、複数のデータベースインスタンスがあり、それぞれにサービスがあるとします。
JonasH

いや。50のテーブルを持つデータベースがあり、50のリポジトリ、50のサービスが必要です。汎用のリポジトリで作業できますが、私のデータベースは正規化されていてクリーンではないため、履歴があるため、リポジトリやサービスには特定のコードが必要です。だから私はすべてのジェネリックを行うことはできません。各サービスには、個別に維持する必要がある特定のビジネスルールがあります。
バスティアンヴァンダム

2

外部システムを分離します。

使用法1:もちろん、インターフェースの実装が複数ある場合にDIを使用するのは論理的であるようです。SQL Serverのリポジトリがあり、次にOracleデータベースのリポジトリがあります。どちらも同じインターフェースを共有し、実行時に必要なインターフェースを「挿入」します(これが使用される用語です)。これはDIではありません。ここでは基本的なOOプログラミングです。


はい、ここでDIを使用します。ネットワーク、データベース、ファイルシステム、別のプロセス、ユーザー入力などに送信される場合は、それを分離する必要があります。

DIを使用すると、これらの外部システムを簡単に模擬できるため、テストが容易になります。いいえ、それが単体テストへの最初のステップであるとは言っていません。これを行わずにテストを行うこともできます。

さらに、データベースが1つしかない場合でも、DIを使用すると移行したい日に役立ちます。だから、はい、DI。

使用法2:特定のメソッドをすべて持つ多くのサービスを持つビジネスレイヤーがある場合、各サービスのインターフェイスを作成し、これが一意であっても実装を注入することをお勧めします。これはメンテナンスに適しているからです。これは私が理解できないこの2番目の使用法です。

もちろん、DIがお手伝いします。コンテナについて議論します。

おそらく、注目に値するのは、具象型を使用した依存性注入が依然として依存性注入であることです。重要なのは、カスタムインスタンスを作成できることです。インターフェースインジェクションである必要はありません(インターフェースインジェクションの方が用途が広いので、どこでも使用できるわけではありません)。

すべてのクラスに対して明示的なインターフェースを作成するという考えは死ぬ必要があります。実際、インターフェースの実装が1つしかない場合... YAGNI。インターフェースの追加は比較的安価で、必要なときに行うことができます。実際、候補の実装が2つまたは3つになるまで待つことをお勧めします。そうすることで、それらの間で何が共通しているかをよりよく理解できます。

ただし、その反対に、クライアントコードが必要とするものにより近いインターフェイスを作成することができます。クライアントコードがクラスの少数のメンバーのみを必要とする場合は、そのためのインターフェイスを持つことができます。これにより、インターフェイスの分離が改善されます


コンテナ?

あなたはそれらを必要としないことを知っています。

それをトレードオフに移しましょう。彼らはそれの価値がない場合があります。あなたはあなたのクラスがコンストラクタに必要な依存関係を取るようにするでしょう。そして、それで十分かもしれません。

私は実際には「セッターインジェクション」の属性にアノテーションを付けるのは好きではありません。サードパーティの属性よりもはるかに少ないので、制御の及ばない実装に必要になるかもしれません。ただし、ライブラリを変更する場合は、これらを変更する必要があります。

最終的には、これらのオブジェクトを作成するためのルーチンの作成を開始します。それを作成するには、まずこれらの他のオブジェクトを作成する必要があり、それらのオブジェクトについてはさらにいくつか必要です...

まあ、それが起こったら、そのロジックをすべて1か所に配置して再利用したいとします。あなたは欲しい真実の単一のソース、あなたのオブジェクトを作成する方法についてを。そして、あなたは自分自身を繰り返さないことによってそれを得ます。これにより、コードが簡略化されます。理にかなっていますよね?

さて、あなたはそのロジックをどこに置きますか?最初の本能は、Service Locatorを用意することです。単純な実装は、ファクトリーの読み取り専用辞書を持つシングルトンです。より複雑な実装では、リフレクションを使用して、ファクトリを提供していない場合にファクトリを作成できます。

ただし、シングルトンまたは静的サービスロケーターvar x = IoC.Resolve<?>を使用すると、インスタンスを作成する必要があるすべての場所で同様のことを行うことになります。これにより、サービスロケータ/コンテナ/インジェクタに強力な結合が追加されます。これは実際に単体テストを困難にする可能性があります。

インスタンス化するインジェクターを使用し、それをコントローラーでのみ使用できるようにします。あなたはそれがコードに深く入りたくありません。実際には、テストが困難になる可能性があります。コードの一部で何かをインスタンス化する必要がある場合は、コンストラクターにインスタンス(またはほとんどのファクトリー)が必要です。

そして、コンストラクターに多くのパラメーターがある場合は、一緒に移動するパラメーターがあるかどうかを確認してください。おそらく、パラメーターを記述子型(理想的には値型)にマージできます。


この明確な説明をありがとうございます。だからあなたに従って私はすべての私のサービスのリストを含むインジェクタークラスを作成する必要があります。サービスごとに注入することはありません。インジェクタークラスを注入します。このクラスは、各サービスの単一インスタンスも管理しますか?
Bastien Vandamme

@BastienVandammeを使用すると、コントローラーをインジェクターと結合できます。したがって、コントローラに渡すことを意味する場合は、同意します。そして、はい、それはサービスの単一のインスタンスを持つことを処理できます。一方、コントローラーがインジェクターを渡しているのではないかと心配します...必要に応じて工場を渡します。実際、アイデアは、サービスとその依存関係の初期化を分離することです。したがって、理想的には、サービスはインジェクターを認識しません。
Theraot

「これらの外部システムを簡単に模擬するため、DIを使用するとテストが容易になります。」最初にシステムに接続するコードを最小化せずに、ユニットテストでこれらのシステムをモックすることに依存すると、ユニットテストの有用性が大幅に低下することがわかりました。まず、入出力テストのみでテストできるコードの量を最大化します。次に、モックが必要かどうかを確認します。
jpmc26

@ jpmc26は同意した。補遺:とにかく、これらの外部システムを分離する必要があります。
Theraot

@BastienVandamme私はこれについて考えてきました。作成するカスタムインジェクターを作成する能力を失うことがなければ、それでうまくいくと思います。
Theraot
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.