私は、いくつかの異なる高レベル関数を持つC#ライブラリの設計について考えています。もちろん、これらの高レベル関数は、可能な限り [〜#〜] solid [〜#〜] クラス設計原則を使用して実装されます。そのため、おそらくコンシューマーが定期的に直接使用することを目的としたクラスと、それらのより一般的な「エンドユーザー」クラスの依存関係である「サポートクラス」があるでしょう。
問題は、次のようにライブラリを設計する最良の方法は何ですか:
私の現在の考えは、一般的なDIライブラリ(StructureMapレジストリ、Ninjectモジュールなど)にいくつかの「DI登録モジュール」、および非DIでこれらの少数の工場へのカップリングを含むセットまたはファクトリクラスを提供することです。
考え?
これは、DIがテクノロジーではなくパターンと原則であることを理解すれば、実際に簡単です。
DIコンテナに依存しない方法でAPIを設計するには、次の一般原則に従います。
実装ではなく、インターフェイスへのプログラム
この原則は、実際には Design Patterns からの(メモリからの)引用ですが、常にあなたの本当の目標でなければなりません。DIは、単にendを達成するための手段です。
ハリウッドの原則を適用する
DIの用語でのハリウッドの原則では、DIコンテナを呼び出さないでください、それはyouを呼び出します。
コード内からコンテナを呼び出して依存関係を直接要求しないでください。 Constructor Injectionを使用して暗黙的に要求します。
コンストラクターインジェクションを使用
依存関係が必要な場合は、コンストラクターでstaticallyを要求します。
public class Service : IService
{
private readonly ISomeDependency dep;
public Service(ISomeDependency dep)
{
if (dep == null)
{
throw new ArgumentNullException("dep");
}
this.dep = dep;
}
public ISomeDependency Dependency
{
get { return this.dep; }
}
}
Serviceクラスがその不変式を保証する方法に注目してください。インスタンスが作成されると、Guard句とreadonly
キーワードの組み合わせにより、依存関係が使用可能になることが保証されます。
短命オブジェクトが必要な場合は、抽象ファクトリを使用します
コンストラクターインジェクションで注入された依存関係は、長期間存続する傾向がありますが、短命のオブジェクトが必要な場合や、実行時にのみ既知の値に基づいて依存関係を構築する場合があります。
詳細については、 this を参照してください。
最後の責任ある瞬間にのみ作成する
最後までオブジェクトを切り離しておきます。通常、アプリケーションのエントリポイントですべてを待機して接続できます。これはComposition Rootと呼ばれます。
詳細はこちら:
ファサードを使用して簡素化する
結果のAPIが初心者ユーザーにとって複雑すぎると感じた場合は、一般的な依存関係の組み合わせをカプセル化する Facade クラスを常に提供できます。
高度な発見可能性を備えた柔軟なファサードを提供するために、Fluent Builderの提供を検討できます。このようなもの:
public class MyFacade
{
private IMyDependency dep;
public MyFacade()
{
this.dep = new DefaultDependency();
}
public MyFacade WithDependency(IMyDependency dependency)
{
this.dep = dependency;
return this;
}
public Foo CreateFoo()
{
return new Foo(this.dep);
}
}
これにより、ユーザーは次のように記述することでデフォルトのFooを作成できます。
var foo = new MyFacade().CreateFoo();
ただし、カスタム依存関係を提供できることは非常にわかりやすく、次のように記述できます。
var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();
MyFacadeクラスが多くの異なる依存関係をカプセル化すると想像するなら、拡張性を発見可能にしながら適切なデフォルトを提供する方法が明確であることを願っています。
FWIW、この答えを書いてからずっと経って、ここで概念を拡張し、 DI-Friendly Libraries についてのより長いブログ投稿と DI-Friendly Frameworks についてのコンパニオン投稿を書きました。
「依存関係の注入」という用語は、IoCコンテナーとは特に関係ありませんが、それらが一緒に言及される傾向があるとしてもです。これは単に、次のようなコードを書く代わりに、ということです:
public class Service
{
public Service()
{
}
public void DoSomething()
{
SqlConnection connection = new SqlConnection("some connection string");
WindowsIdentity identity = WindowsIdentity.GetCurrent();
// Do something with connection and identity variables
}
}
次のように書きます。
public class Service
{
public Service(IDbConnection connection, IIdentity identity)
{
this.Connection = connection;
this.Identity = identity;
}
public void DoSomething()
{
// Do something with Connection and Identity properties
}
protected IDbConnection Connection { get; private set; }
protected IIdentity Identity { get; private set; }
}
つまり、コードを作成するときに2つのことを行います。
実装を変更する必要があると思われる場合は、クラスではなくインターフェイスに依存してください。
これらのインターフェイスのインスタンスをクラス内に作成する代わりに、コンストラクター引数として渡します(または、パブリックプロパティに割り当てることができます。前者はconstructor injection 、後者はproperty injection)です。
これは、DIライブラリの存在を前提とするものではなく、実際にコードなしでコードを記述することを難しくするものではありません。
この例を探している場合は、.NET Framework自体を探す必要があります。
List<T>
はIList<T>
を実装します。 IList<T>
(またはIEnumerable<T>
)を使用するようにクラスを設計すると、Linq to SQL、Linq to Entities、およびNHibernateがすべて通常は背後で行うように、遅延読み込みなどの概念を利用できます。プロパティの注入を通じて。一部のフレームワーククラスは、いくつかのデータバインディング機能に使用されるIList<T>
など、BindingList<T>
をコンストラクター引数として実際に受け入れます。
Linq to SQLおよびEFは、IDbConnection
および関連するインターフェイスを中心に構築されており、パブリックコンストラクターを介して渡すことができます。ただし、それらを使用する必要はありません。デフォルトのコンストラクタは、設定ファイル内のどこかに接続文字列があれば問題なく動作します。
WinFormsコンポーネントで作業する場合、INameCreationService
やIExtenderProviderService
などの「サービス」を扱います。具象クラスが何であるかさえ、あなたは本当に知りません。 .NETには、これに使用される独自のIoCコンテナIContainer
があり、Component
クラスには、実際のサービスロケーターであるGetService
メソッドがあります。もちろん、IContainer
またはその特定のロケーターなしでこれらのインターフェイスの一部またはすべてを使用することを妨げるものはありません。サービス自体は、コンテナと疎結合しているだけです。
WCFのコントラクトは、完全にインターフェイスを中心に構築されています。実際の具体的なサービスクラスは通常、構成ファイル内の名前で参照されます。これは基本的にDIです。多くの人はこれを認識していませんが、この構成システムを別のIoCコンテナーと交換することは完全に可能です。おそらくもっと興味深いことに、サービスの動作はすべて、後で追加できるIServiceBehavior
のインスタンスです。繰り返しになりますが、これを簡単にIoCコンテナーに配線し、関連する動作を選択させることができますが、この機能は1つなくても完全に使用可能です。
などなど。 DIは.NETのあちこちにありますが、通常はシームレスに行われているため、DIとは見なされません。
最大限の使いやすさのためにDI対応ライブラリを設計したい場合、おそらく最良の提案は、軽量コンテナを使用して独自のデフォルトIoC実装を提供することです。 IContainer
は.NET Framework自体の一部であるため、これに最適です。
EDIT 2015:時間が経ちましたが、今ではこの全体が大きな間違いであることがわかりました。 IoCコンテナはひどく、DIは副作用に対処するための非常に悪い方法です。事実上、ここでのすべての回答(および質問自体)は避ける必要があります。副作用に注意し、それらを純粋なコードから分離するだけで、他のすべてが適切に配置されるか、無関係で不要な複雑さのいずれかになります。
元の答えは次のとおりです。
SolrNet の開発中に、この同じ決定に直面しなければなりませんでした。私は、DIフレンドリーでコンテナに依存しないという目標から始めましたが、内部コンポーネントを追加するにつれて、内部工場はすぐに管理不能になり、結果のライブラリは柔軟性がなくなりました。
私は自分で 非常に単純な埋め込みIoCコンテナ を書きながら、 Windsor機能 と Ninjectモジュール も提供しました。ライブラリを他のコンテナと統合することは、コンポーネントを適切に配線するだけの問題であるため、Autofac、Unity、StructureMapなどと簡単に統合できます。
この欠点は、サービスをnew
だけ上げる能力を失ったことです。 CommonServiceLocator への依存関係も取りましたが、これは回避できました(将来リファクタリングする可能性があります)。これにより、組み込みコンテナーの実装が容易になります。
詳細はこちら ブログ投稿 。
MassTransit は同様のものに依存しているようです。実際にはCommonServiceLocatorのIServiceLocatorであり、さらにいくつかのメソッドを持つ IObjectBuilder インターフェイスがあります。その後、各コンテナにこれを実装します。つまり、 NinjectObjectBuilder と通常のモジュール/ファシリティ、 MassTransitModule 。次に、 IObjectBuilderに依存 必要なものをインスタンス化します。もちろん、これは有効なアプローチですが、個人的には、コンテナをサービスロケータとして使用して、コンテナを実際に通過しすぎているため、あまり好きではありません。
MonoRail を実装 独自のコンテナー も同様に、古き良きものを実装 IServiceProvider 。このコンテナは、このフレームワーク全体で 既知のサービスを公開するインターフェイス を介して使用されます。具体的なコンテナを取得するには、 組み込みのサービスプロバイダーロケーター を使用します。 Windsor facility は、このサービスプロバイダーロケーターをWindsorにポイントし、選択したサービスプロバイダーにします。
結論:完璧な解決策はありません。設計上の決定と同様に、この問題には柔軟性、保守性、利便性のバランスが必要です。
私がすることは、DIコンテナに依存しない方法でライブラリを設計し、コンテナへの依存を可能な限り制限することです。これにより、必要に応じてDIコンテナを別のコンテナに交換できます。
次に、DIロジックの上にあるレイヤーをライブラリのユーザーに公開して、インターフェイスで選択したフレームワークを使用できるようにします。これにより、公開したDI機能を引き続き使用でき、他のフレームワークを独自の目的に自由に使用できます。
ライブラリのユーザーが独自のDIフレームワークをプラグインできるようにすることは、メンテナンスの量が劇的に増加するため、少し間違っているように思えます。これは、そのままのDIよりもプラグイン環境になります。