web-dev-qa-db-ja.com

依存性注入における循環依存性

dIはかなり新しく、Prism/Unityを使用するようにアプリケーションをリファクタリングしています。私たちはほとんどそこにいますが、循環依存関係に行き詰まっています。私はたくさん読んでいます、同様の質問がたくさんありますが、私たちの状況で「正しい」解決策が何であるかについては疑問があります。

アプリケーションはいくつかのIDEに似ています。プロジェクトエクスプローラー、検索、ドキュメントなどがあります。ドキュメントは、実際にはINodeViewModelのノードを持つツリービューです。コードサンプルは実際のコードの最小化されたバージョンであることに注意してください。

ツールとドキュメントアイテムを管理するIDockingServiceがあります。

public interface IDockingService
{
    INodeViewModelNavigationService NodeViewModelNavigationService { get; }

    ExplorerViewModel ExplorerViewModel { get; }
    SearchViewModel SearchViewModel { get; }

    ReadOnlyCollection<ToolItemViewModel> ToolItemViewModels { get; }
    ReadOnlyCollection<DocumentItemViewModel> DocumentItemViewModels { get; }
}

そして、すべてのINodeViewModelを管理し、モデルと接続するISpecificationViewModelServiceがあります。

public interface ISpecificationViewModelService
{
    INodeViewModel RootX { get; }
    INodeViewModel RootY { get; }
    INodeViewModel RootZ { get; }
}

矛盾する2つの要件があります。

  • 新しいノードが作成されたら、そこに移動します。現在、_IDockingServiceを具体的なSpecificationViewModelService実装にコンストラクター注入を介して渡し、さらに各NodeViewModelに渡します。
  • DockingServiceの特定のツールは、ISpecificationViewModelServiceについて知る必要があります。たとえば、エクスプローラーはすべてのルートを表示する必要があります。

DockingItemServiceは、エクスプローラーなどのツールアイテムを作成および管理するため、ISpecificationViewModelServiceが必要です。ただし、ISpecificationViewModelServiceがノードに移動するには、IDockingServiceが必要です。循環障害。

ツールアイテムをコンポジションルートに作成させたので、最初の試行ではこの問題はありませんでしたが、それは正しくないようでした。一緒にうまく管理できなかったインスタンス。

読んでみると、ファクトリー(または別の3番目のクラス)がここで役立つことを理解しています。まだどうなのかは分からない。問題は、DockingServiceがツールアイテムを作成するためにSpecificationViewModelServiceを必要とするだけであり、逆も同様であることは理解していると思います。工場はこの循環的な問題を取り除くことができますが、それが正しい解決策であるかどうかはわかりません。コンテナーはコンポジションルートでのみ使用できますが、コンテナ(コンテナーが必要ですか?)

この問題を処理する正しい方法は何でしょうか?

6
Jef Patat

私はあなたの状況からかなり抽象化します。これがあなたの問題の最善の解決策になるかどうかはわかりません。 aソリューションになりますが、貧弱な設計を可能にするだけかもしれません。言うのは難しいですが、確かに状況は合理的な設計で生じます。

XをビルドするにはYが必要で、YをビルドするにはXが必要になるという状況がよくあります。もちろん、Xを構築するために概念的にはYを完全に構築する必要はありません。逆に、システムを構築することは論理的に不可能です。代わりに、実際に意味するのは、一方を構築することであり、もう一方を参照する方法だけが必要です。まだ作成されていないものを参照する方法はたくさんあります。たとえば、Cでは、割り当てられているが初期化されていないメモリへのポインタを渡すか、レジストリで作成されたオブジェクトを検索するために使用される文字列を渡すことができます。 HaskellやScalaのような言語では、これを解決する1つの方法は遅延評価です。実装面では、これは多かれ少なかれ、内部の変更可能な状態で高次関数を渡すこと、または同等に、依存関係の解決をメモする「ファクトリー」オブジェクトと同じです。これが私がここで提案するアプローチです。以下の Autofac の用語とAPIを使用しますが、このアイデアは他の依存関係注入フレームワークに簡単に適用できるはずです。

_class Lazy<T> where T : class {
    private T _cached = null;
    private IContainer _container;
    public Lazy(Func<IContainer> container) {
        _container = container();
    }
    public T Value {
        get {
            if(_cached != null) return _cached;
            _cached = _container.Resolve<T>();
            _container = null;
            return _cached;
        }
    }
}
_

ここでIContainerは依存関係注入コンテナであることを意図しており、Resolveは依存関係ルックアップを実行します。私はstrongly依存関係注入コンテナーをオブジェクトに渡さないことをお勧めしますが、この場合、依存関係注入の一部としてLazyを表示していますフレームワーク。実際、依存性注入フレームワークがそのような機能をまだ提供していないことを確認する必要があります。代わりに、遅延注入するクラスごとにファクトリを作成することもできます。 IContainerを使用してオブジェクトを作成する代わりにResolveを使用する代わりに、newを依存関係で置き換える以外は、コードは基本的に同じです。あるいは、Lazyを一般化して_Action<T>_を取り、それをメモするだけで、これらのファクトリオブジェクトは、次のようにコンポジションルートに登録するシングルトンになります:container.Register(new Lazy<Foo>(() => new Foo()));理想状況は オープンジェネリックを登録する タイプ_Lazy<>_になるため、1回の登録ですべてのケースを処理できます。 (Autofacでこれが機能することを確認しました。)

これの結果は、IFooではなく_Lazy<IFoo>_に依存し、Valueプロパティを介してアクセスすることになります(ただし、コンストラクターではありません)。

本当に必要な場合は、別の解決策として、コンストラクターインジェクションを削除し、IServiceProviderコンストラクターインジェクションを追加し、次のようなドッキングサービスが必要なときにActivatorUtilitiesを使用します。

var dockingService = (IDockingService)ActivatorUtilities.CreateInstance(this.serviceProvider, typeof(DockingService));

もちろん、これは推奨される方法ではなく、適切にテストすることはできません(具体的なIDockingServiceをインスタンス化しているため)が、これは解決策です。

0
ozba