web-dev-qa-db-ja.com

依存関係注入(C#)を使用する必要がある場合

依存性注入(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は私のコードを簡素化します。信じがたい。

8

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

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

したがって、DIは可読性を向上させるためにnotではなく、テスト性を向上させるために使用されます(もちろん、排他的ではありません)。

これはそれ自体が目的ではありません。クラスItemServiceが非常に単純なクラスである場合、これは外部ネットワークまたはデータベースへのアクセスを行わないため、SiteServiceなどの単体テストの記述を妨げることはなく、後者を個別にテストします努力する価値があるので、DIは必要ありません。ただし、ItemServiceがネットワーク経由で他のサイトにアクセスしている場合、おそらくSiteServiceを切り離して単体テストする必要があります。これは、「実際の」ItemServiceをa MockItemService。ハードコードされたいくつかの偽のアイテムを提供します。

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

14
Doc Brown

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

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

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

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

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

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

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

4
candied_orange

DIのヘリコプタービューは、単純にインターフェイスの実装をスワップアウトする機能です。もちろん、これはテストの恩恵ですが、他にも潜在的な利点があります。

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

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

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

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

2
Robbie Dee

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コンテナーを構成することもできます。

2
JonasH

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

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


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

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

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

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

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

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

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

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


コンテナ?

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

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

私は実際には「セッター注入」の属性に注釈を付けることのファンではありません。サードパーティの属性よりもはるかに少ないので、コントロールの及ばない実装に必要になるかもしれません…しかし、変更することに決めた場合ライブラリは変更する必要があります。

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

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

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

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

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

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

2
Theraot