web-dev-qa-db-ja.com

内部依存関係のあるライブラリの依存関係注入

背景

私はWebサイトをサポートするためにクラスライブラリに取り組んでいます。このライブラリは、いくつかの異なるベンダーからの関連するAPIを組み合わせたもので、すべて独自の特定のニュアンスとドメインオブジェクトを持っています。例として、BankAccountオブジェクトを公開するクレジットカードプロバイダー、Accountを公開するローンプロバイダー、およびAccountDtoを公開する当座預金システムがあるとします。ライブラリの主な設計目標は、これらのさまざまな実装を非表示にし、WebサイトがAccountの単一の概念で機能できるようにすることです。呼び出し元のベンダーとデータの調整方法を決定する一連のサービスを提供しますさまざまなソース。

混乱を隠してAPIをクリーンに保つために、自分のドメインオブジェクトとサービスだけがpublicです。ライブラリ内には、各ベンダーが独自の repository およびドメインオブジェクトを持っています。ベンダーのクラスにはすべてinternalのマークが付けられており、Web開発者がサービスレイヤーを(意図的または非意図的に)迂回するのを防ぎます。

したがって、フローは次のようになります。

Webサイト>> My API >>サービスレイヤー(パブリック)>>リポジトリレイヤー(内部)

このソリューションでは、依存関係の注入とUnity IoCコンテナーを使用します。私のAPIのサービスはコンポジションルートに登録され、コンストラクターの引数としてWebサイトのコントローラーに直接注入されます。これらはすべて正常に機能します。

問題

これが私が苦しんでいる問題です。私のサービスには、リポジトリをサービスに注入できるコンストラクタ引数があります。しかし、リポジトリはinternalであるため、Webサイトはそれらをコンポジションルートに登録できません(Reflectionで本当に奇妙なことをしないと)。加えて、私はウェブサイトの開発者にそれらを注入することについて心配してほしくない。

だから問題は:DIアプローチで受け入れられている規約を遵守しながら、内部/プライベートリポジトリをパブリックサービスレイヤーにどのように注入するのですか?

私の提案するソリューション

私が今これをやっている方法は次のとおりです:

抽象RepositoryLocatorインターフェイスとクラスをライブラリに追加しました。独自のUnityコンテナーがあります。単体テストプロジェクトが独自のバージョンを導出し、スタブリポジトリを置き換えることができるように、それは封印されていないままです。

public interface IRepositoryLocator
{
    T GetRepository<T>() where T : IRepository;
}

public abstract class BaseRepositoryLocator : IRepositoryLocator
{
    protected readonly UnityContainer _container = new UnityContainer();

    public virtual T GetRepository<T>() where T : IRepository
    {
        return _container.Resolve<T>();
    }
}

次に、DefaultRepositoryLocatorをライブラリに追加して、本番ランタイムリポジトリを提供するようにハードコーディングしました。

public sealed class DefaultRepositoryLocator : BaseRepositoryLocator
{
    public DefaultRepositoryLocator()
    {
        RegisterDefaultTypes();
    }
    private void RegisterDefaultTypes()
    {
        _container.RegisterType<IVendorARepository, VendorARepository>();
        _container.RegisterType<IVendorBRepository, VendorBRepository>();
        _container.RegisterType<IVendorCRepository, VendorCRepository>();
    }
}

次に、サービスクラスで、リポジトリをインジェクトする代わりに、リポジトリlocatorをインジェクトし、コンストラクターで必要なリポジトリを引き出します。

public class AccountService : IAccountService
{
    internal readonly IVendorARepository _vendorA;
    internal readonly IVendorBRepository _vendorB;

    public AccountService(IRepositoryLocator repositoryLocator)
    {
        _vendorA = repositoryLocator.GetRepository<IVendorARepository>();
        _vendorB = repositoryLocator.GetRepository<IVendorBRepository>();
    }
}

Web開発チームの唯一のタスク

Webサイトのコンポジションルートでは、Unityコンテナーにデフォルトのリポジトリロケーターを登録するだけです。

container.RegisterType<IRepositoryLocator, DefaultRepositoryLocator>();

それが完了すると、Unityコンテナーはリポジトリロケーターをコントローラーに注入されるすべてのサービスに注入し、サービスは必要なリポジトリを取得できます。

単体テスト

ユニットテストプロジェクトでは、ユニットテストアセンブリに InternalsVisibleTo属性 を使用して内部メンバーへのアクセス権が与えられ、テストプロジェクトは内部に準拠した独自のStubRepositoryLocatorに置き換えます-now-publicインターフェース。

このデザインについて面倒なこと

IoCエキスパートの皆さん、どのような問題が発生しましたか?これが私が心配することです

  1. Unityコンテナーは2つあり、1つはWebアプリケーションにあり、もう1つはライブラリのRepositoryLocatorにあります。たった一つだけにすべきだと聞いたのですが、それを回すのはアンチパターンだとも書いています。
  2. RepositoryLocatorは、本番ランタイム用にハードコードされています。多くのエンジニアがそうであるように、ハーコーディングは主要な罪ではないと思いますが、私はそれを疑っていると思います。
  3. 単体テストプロジェクトでは、ライブラリに InternalsVisibleToAttribute を追加する必要があります。これは間違いなくコードのにおいであり、単体テストプロジェクトの名前が変更された場合や、複数のプロジェクトが必要になった場合にどのように対処するのでしょうか。

  4. 私のサービスのコンストラクターは、個々のリポジトリーを個別に注入するのではなく、RepositoryLocator自体を1回注入します。これにより、コンパイル時に個々の依存関係が隠されます。

これらの問題を解決し、IoCのベストプラクティスに準拠しながら、internalスコープの背後にリポジトリレイヤーを隠蔽するアプローチはありますか?

9
John Wu

Webサイトのコンポジションルートでは、デフォルトのリポジトリロケーターを登録するだけです

IMOウェブサイトはあなたのリポジトリについて知る必要さえありません。あなたが言ったように、リポジトリとベンダーのクラスは内部にあるので、あなたのサイトはそれらへのアクセスやどこかにそれらを注入することについて心配するべきではありません。繰り返しますが、あなたが言ったように、複数のベンダーを抽象化し、よりシンプルなAPIをユーザー(例ではWebサイト)に公開するのはライブラリの役割です。

そうは言っても、私はあなたがすべきだと思います:

  • APIで、ライブラリが適切に機能するためにクライアントが一度実行する必要があるいくつかの初期エントリポイントメソッドを提供します。
  • このメソッド内(ライブラリ内)で、ベンダーのリポジトリクラス(ライブラリの内部)を使用してコンテナーを構成します。
  • ベンダーのものを非公開にし、サービスを公開する
  • それだけです。クライアント(Webサイト)は、サービスの注入についてのみ心配する必要があります。それ以上のものはありません。
  • オプション:構成ファイルを使用してインターフェースと実装をバインドできます

ご質問・ご心配について:

  1. あなたは(通常)あなたのクライアントが何をするかについて制御することはできません。クライアントアプリは、クラスを挿入することすらせず、すぐに使用することができます。コメントで言ったように、心配しないでください。
  2. 構成ファイルは、このためのより良いアプローチであり、運用環境またはテスト環境で簡単に変更できます。
  3. 単体テストの目的に関しては、この属性に問題はありません。
  4. 私の提案はこれを解決します
2
Emerson Cardoso

コンテナー拡張 を使用できます。これにより、内部クラスを含むアセンブリは、コンシューマがそれらについて知って構成する必要なく、独自の依存関係を構成できます。

ドキュメントは次のように示しています:

IUnityContainer container = new UnityContainer();
container.AddNewExtension<MyCustomExtension>();

コンシューマは拡張機能を追加するだけで、その拡張機能クラスが詳細を処理します。

1
Scott Hannen