Dependency Inject(DI)「フレンドリーな」ライブラリ


230

私は、いくつかの異なる高水準関数を持つC#ライブラリの設計について考えています。もちろん、これらの高水準関数は、可能な限りSOLIDクラスの設計原則を使用して実装されます。そのため、コンシューマが定期的に直接使用することを目的としたクラスと、より一般的な「エンドユーザー」クラスの依存関係である「サポートクラス」が存在する可能性があります。

問題は、ライブラリを設計する最良の方法は何ですか?

  • DIに依存しない-1つまたは2つの一般的なDIライブラリ(StructureMap、Ninjectなど)に基本的な「サポート」を追加するのは妥当なようですが、コンシューマが任意のDIフレームワークでライブラリを使用できるようにしたいと思います。
  • 非DI使用可能-ライブラリのコンシューマーがDIを使用していない場合でも、ライブラリは可能な限り使いやすく、これらの「重要でない」依存関係をすべて作成するためにユーザーが実行する必要がある作業量を削減する必要があります。彼らが使いたい「本物の」クラス。

私の現在の考えは、一般的なDIライブラリー(たとえば、StructureMapレジストリー、Ninjectモジュール)にいくつかの「DI登録モジュール」、および非DIであり、これらのいくつかのファクトリーへのカップリングを含むセットまたはファクトリー・クラスを提供することです。

考え?


リンク切れ記事(SOLID)への新しい参照:butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

回答:


360

DIはパターンではなく技術であり、テクノロジーではないことを理解すれば、これは実際には簡単です。

DIコンテナーに依存しない方法でAPIを設計するには、以下の一般的な原則に従います。

実装ではなくインターフェースへのプログラム

この原則は、実際にはDesign Patternsからの(ただし、メモリからの)引用ですが、常にあなたの真の目標である必要があります。DIは、その目的を達成するための手段にすぎません

ハリウッド原則を適用する

DI用語でのハリウッド原則によると、DIコンテナーを呼び出さないでください

コード内からコンテナーを呼び出して、依存関係を直接要求しないでください。Constructor Injectionを使用して暗黙的に要求します。

コンストラクターインジェクションを使用する

依存関係が必要な場合は、コンストラクターを介して静的に依存関係を要求します。

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キーワードの組み合わせにより、依存関係が利用できることが保証されます。

寿命の短いオブジェクトが必要な場合は、抽象ファクトリを使用します

コンストラクターインジェクションを使用して注入された依存関係は、存続期間が長くなる傾向がありますが、存続期間の短いオブジェクトが必要な場合や、実行時にのみ既知の値に基づいて依存関係を構成する場合があります。

詳細については、こちらをご覧ください。

最後の責任ある瞬間にのみ作成する

最後までオブジェクトを切り離してください。通常、アプリケーションのエントリポイントですべてを待機して配線できます。これは、コンポジションルートと呼ばれます。

詳細はこちら:

ファサードを使用して簡素化する

結果のAPIが初心者ユーザーにとって複雑すぎると感じる場合は、一般的な依存関係の組み合わせをカプセル化するいくつかのFacadeクラスを常に提供できます。

柔軟なファサードに高度な発見可能性を提供するために、フルーエントビルダーの提供を検討できます。このようなもの:

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に関する関連記事を書きました。


21
これは紙の上では素晴らしいように思えますが、私の経験では、多くの内部コンポーネントが複雑な方法で相互作用していると、管理する工場が多くなり、メンテナンスが困難になります。さらに、工場は作成されたコンポーネントのライフスタイルを管理する必要があります。ライブラリを実際のコンテナーにインストールすると、コンテナー自体のライフスタイル管理と競合します。工場やファサードは、実際のコンテナの邪魔になります。
Mauricio Scheffer

4
これをすべて行うプロジェクトはまだ見つけていません。
Mauricio Scheffer

31
まあ、それがSafewhereでソフトウェアを開発する方法なので、あなたの経験は共有しません...
Mark Seemann

19
ファサードはコンポーネントの既知の(おそらく一般的な)組み合わせを表すため、ファサードは手動でコーディングする必要があると思います。すべてを手作業で接続できるため、DIコンテナーは必要ありません(Poor ManのDIを考えてください)。Facadeは、APIのユーザーにとっては単なるオプションの便利なクラスであることを思い出してください。上級ユーザーは、ファサードをバイパスしてコンポーネントを自分の好みに合わせて配線したい場合があります。彼らはこれのために独自のDIコンテナを使用したいと思うかもしれないので、もし彼らがそれを使用しないなら、特定のDIコンテナを彼らに強制するのは不親切だと思います。可能ですがお勧めできません
Mark Seemann 2010年

8
これは、私がSOでこれまでに見た中で唯一の最良の答えかもしれません。
Nick Hodges 2013年

40

「依存関係の注入」という用語は、一緒に言及される傾向があるにもかかわらず、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つのことを行います。

  1. 実装を変更する必要があると思われる場合は、クラスではなくインターフェイスに依存してください。

  2. これらのインターフェイスのインスタンスをクラス内に作成する代わりに、コンストラクターの引数として渡します(または、それらをパブリックプロパティに割り当てることができます。前者はコンストラクターインジェクションで、後者はプロパティインジェクションです)。

これは、DIライブラリの存在を前提とするものではありません。また、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コンテナーに簡単に接続して、関連する動作を選択させることもできますが、この機能は何もしなくても完全に使用できます。

などなど。.NETのいたるところにDIがありますが、それは通常、シームレスに行われるため、DIとは考えられません。

最大限の使いやすさのためにDI対応のライブラリを設計したい場合は、おそらく軽量コンテナを使用して独自のデフォルトIoC実装を提供することをお勧めします。 IContainer.NET Framework自体の一部であるため、これは最適な選択肢です。


2
コンテナの実際の抽象化は、IContainerではなくIServiceProviderです。
Mauricio Scheffer、2010年

2
@Mauricio:もちろんですが、LWCシステムを使用したことIContainerがない人に、なぜ実際には1段落のコンテナではないのかを説明してみてください。;)
アーロノート

アーロン、プライベートセットを使用する理由に興味があります。フィールドを読み取り専用として指定する代わりに?
jr3

@Jreeter:なぜprivate readonlyフィールドではないのですか?それらが宣言クラスでのみ使用されているが、OPがこれがフレームワーク/ライブラリレベルのコードであると指定した場合、これはすばらしいことであり、サブクラス化を意味します。このような場合、通常、サブクラスに公開される重要な依存関係が必要になります。明示的なゲッター/セッターを使用してprivate readonlyフィールドプロパティを作成することもできましたが、例ではスペースが無駄になり、実際に維持するコードが増えましたが、実際の利益はありません。
アーロンノート、2013年

明確にしていただきありがとうございます!子供は読み取り専用フィールドにアクセスできると思いました。
jr3 2013年

5

EDIT 2015:時間が経ちましたが、今、この全体が大きな間違いだったことがわかりました。IoCコンテナはひどいものであり、DIは副作用に対処するための非常に貧弱な方法です。事実上、ここでのすべての回答(および質問自体)は回避されます。単に副作用に注意し、それらを純粋なコードから分離してください。そうしないと、その他すべてが適切に機能するか、無関係で不要な複雑さになります。

元の答えは次のとおりです:


SolrNetを開発している間、私は同じ決定に直面しなければなりませんでした。私はDIフレンドリーでコンテナにとらわれないという目標から始めましたが、内部コンポーネントをどんどん追加していくと、内部ファクトリーはすぐに管理できなくなり、結果として得られるライブラリーに柔軟性がなくなりました。

結局、独自の非常にシンプルな組み込みIoCコンテナーを作成する一方で、Windsor機能Ninjectモジュールも提供しました。ライブラリを他のコンテナと統合することは、コンポーネントを適切に配線することだけの問題なので、Autofac、Unity、StructureMapなどと簡単に統合できます。

これの欠点はnew、サービスを上げるだけの能力を失ったことです。また、埋め込みコンテナーをより簡単に実装できるようにするために、回避できたCommonServiceLocatorへの依存関係(将来的にリファクタリングする可能性があります)も取り入れました。

詳細については、このブログ投稿をご覧ください。

MassTransitも同様の機能に依存しているようです。これには、実際にはCommonServiceLocatorのIServiceLocatorであるIObjectBuilderインターフェイスがあり、さらにいくつかのメソッドがあります。これは、各コンテナー、つまりNinjectObjectBuilderと通常のモジュール/機能、つまりMassTransitModuleに対してこれを実装します。次に、IObjectBuilder依存して、必要なものをインスタンス化します。これはもちろん有効な方法ですが、実際にはコンテナをサービスロケータとして使用しすぎているため、個人的にはあまり好きではありません。

モノレールは、実装独自のコンテナを古き良き実装するだけでなくIServiceProviderを。このコンテナは、よく知られたサービスを公開するインターフェースを通じてこのフレームワーク全体で使用されます。具体的なコンテナを取得するために、サービスプロバイダロケータが組み込まれていますウィンザー施設は、それ選択されたサービスプロバイダ作り、ウィンザーにこのサービスプロバイダロケータを指します。

結論:完璧なソリューションはありません。他の設計決定と同様に、この問題には柔軟性、保守性、および利便性のバランスが必要です。


12
私はあなたの意見を尊重しますが、あなたの過度に一般化された「ここでの他のすべての回答は時代遅れであり、避けられるべきである」と非常に素朴だと思います。それ以来何かを学んだら、依存性注入が言語の制限に対する答えであるということです。現在の言語を進化または放棄することなく、アーキテクチャをフラット化してモジュール性を実現するには、DI が必要になるようです。一般的な概念としてのIoCは、これらの目標の結果であり、間違いなく望ましいものです。IoC コンテナはIoCにもDIにも絶対に必要ではなく、それらの有用性はモジュールの構成に依存します。
TNE

@tne私が「非常にナイーブ」であるとのあなたの言及は、歓迎されないアドホミネムです。C#、VB.NET、JavaでDIまたはIoCを使用せずに複雑なアプリケーションを記述しました(説明から判断すると、IoCが必要だと思われる言語)。これは、言語の制限に関するものではありません。概念的な制限についてです。アドホミネム攻撃や不十分な議論に頼るのではなく、関数型プログラミングについて学ぶことをお勧めします。
Mauricio Scheffer、2015年

18
私が言及した私は、あなたの見つけ文はナイーブであることを。これは個人的にあなたについて何も言わず、すべて私の意見です。ランダムな見知らぬ人からの侮辱を感じないでください。関連するすべての関数型プログラミングアプローチについて無知であることを暗示することも役に立ちません。あなたはそれを知りません、そしてあなたは間違っているでしょう。私はあなたがそこに精通していることを知っいました(すぐにあなたのブログを見てください)。IOCは関数型プログラミング(..)で、文字通りどこでも起こる
TNE

4
そして一般的に高レベルのソフトウェアで。実際、関数型言語(および他の多くのスタイル)は、DIなしでIoCを解決することを実際に証明しています。私たちは両方ともそれに同意していると思います。あなたが言及する言語でDIなしで管理した場合、どのようにそれをしましたか?私はを使用していないと思いServiceLocatorます。機能的手法を適用すると、依存関係が注入されたクラスであるクロージャーと同等の結果になります(依存関係は閉じられた変数です)。他には?(私はどちらかDI好きではないので、私は、純粋に興味があります。)
TNE

1
IoCコンテナーはグローバルな可変ディクショナリーであり、パラメトリック性を推論のツールとして取り除いているため、避ける必要があります。「私たちに電話しないでください、私たちはあなたに電話します」のようなIoC(概念)は、関数を値として渡すたびに技術的に適用されます。DIの代わりに、ADTを使用してドメインをモデル化してから、インタープリターを記述します(無料を参照)。
Mauricio Scheffer、2015年

1

私が行うことは、コンテナーへの依存をできるだけ制限するために、DIコンテナーにとらわれない方法でライブラリーを設計することです。これにより、必要に応じてDIコンテナーを別のコンテナーに交換できます。

次に、DIロジックの上のレイヤーをライブラリのユーザーに公開し、ユーザーがインターフェイスを介して選択したフレームワークを使用できるようにします。このようにして、公開したDI機能を引き続き使用でき、他のフレームワークを自分の目的に自由に使用できます。

ライブラリのユーザーが自分のDIフレームワークをプラグインできるようにすると、メンテナンスの量が劇的に増えるため、私には少し間違っているように見えます。これは、単純なDIよりもプラグイン環境になります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.