背景
私は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エキスパートの皆さん、どのような問題が発生しましたか?これが私が心配することです
RepositoryLocator
にあります。たった一つだけにすべきだと聞いたのですが、それを回すのはアンチパターンだとも書いています。単体テストプロジェクトでは、ライブラリに InternalsVisibleToAttribute を追加する必要があります。これは間違いなくコードのにおいであり、単体テストプロジェクトの名前が変更された場合や、複数のプロジェクトが必要になった場合にどのように対処するのでしょうか。
私のサービスのコンストラクターは、個々のリポジトリーを個別に注入するのではなく、RepositoryLocator
自体を1回注入します。これにより、コンパイル時に個々の依存関係が隠されます。
これらの問題を解決し、IoCのベストプラクティスに準拠しながら、internal
スコープの背後にリポジトリレイヤーを隠蔽するアプローチはありますか?
Webサイトのコンポジションルートでは、デフォルトのリポジトリロケーターを登録するだけです
IMOウェブサイトはあなたのリポジトリについて知る必要さえありません。あなたが言ったように、リポジトリとベンダーのクラスは内部にあるので、あなたのサイトはそれらへのアクセスやどこかにそれらを注入することについて心配するべきではありません。繰り返しますが、あなたが言ったように、複数のベンダーを抽象化し、よりシンプルなAPIをユーザー(例ではWebサイト)に公開するのはライブラリの役割です。
そうは言っても、私はあなたがすべきだと思います:
ご質問・ご心配について:
コンテナー拡張 を使用できます。これにより、内部クラスを含むアセンブリは、コンシューマがそれらについて知って構成する必要なく、独自の依存関係を構成できます。
ドキュメントは次のように示しています:
IUnityContainer container = new UnityContainer();
container.AddNewExtension<MyCustomExtension>();
コンシューマは拡張機能を追加するだけで、その拡張機能クラスが詳細を処理します。