web-dev-qa-db-ja.com

依存関係注入フレームワークの引数の1つに疑問を投げかける:オブジェクトグラフの作成が難しいのはなぜですか?

Google Guiceのような依存性注入フレームワークは、その使用法に次の動機を与えます( source ):

オブジェクトを構築するには、まずその依存関係を構築します。しかし、各依存関係を構築するには、その依存関係が必要になります。したがって、オブジェクトを作成するときは、オブジェクトグラフを作成する必要があります。

手作業でオブジェクトグラフを作成すると、労働集約型となり(...)、テストが困難になります。

しかし、私はこの引数を購入しません。依存性注入フレームワークがなくても、インスタンス化が簡単で、テストにも便利なクラスを作成できます。例えば。 Guice motivation page の例は、次のように書き直すことができます。

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

したがって、依存性注入フレームワークには他の引数があるかもしれません(これはこの質問の範囲外です!)が、テスト可能なオブジェクトグラフの簡単な作成は1つではありませんそれらのうち、それは何ですか?

13
oberlies

「自分のコラボレーターをインスタンス化する」アプローチは、依存関係treesで機能する可能性がありますが、一般的な依存性グラフ有向非巡回グラフ(DAG)では確実に機能しません。ディペンデンシーDAGでは、複数のノードが同じノードを指すことができます。つまり、2つのオブジェクトが同じオブジェクトをコラボレーターとして使用します。このケースは、質問で説明されているアプローチでは実際には構築できません。

コラボレーターの一部(またはコラボレーターのコラボレーター)が特定のオブジェクトを共有する必要がある場合、このオブジェクトをインスタンス化してコラボレーターに渡す必要があります。だから私は実際には私の直接の協力者以上のものを知る必要があるでしょう、そしてこれは明らかにスケーリングしません。

4
oberlies

依存性注入を行うための最良の方法については、古くて古い進行中の議論があります。

  • 元の春のカットはプレーンオブジェクトをインスタンス化し、セッターメソッドを通じて依存関係を注入しました。

  • しかし、多くの不測の事態が発生すると、コンストラクターのパラメーターを介して依存関係を注入することが正しい方法であると主張しました。

  • その後、最近、リフレクションの使用がより一般的になるにつれて、セッターやコンストラクター引数を使用せずにプライベートメンバーの値を直接設定することが大流行しました。

したがって、最初のコンストラクターは、依存関係注入に対する2番目のアプローチと一致しています。それはあなたがテストのためにモックを注入するような素晴らしいことをすることを可能にします。

しかし、引数のないコンストラクタにはこの問題があります。 PaypalCreditCardProcessorDatabaseTransactionLogの-​​implementationクラスをインスタンス化しているので、Paypalとデータベースへのコンパイル時のハードな依存関係が作成されます。依存関係ツリー全体を正しく構築および構成する責任があります。

  • PayPayプロセッサが非常に複雑なサブシステムであり、さらに多くのサポートライブラリを取り込むと想像してください。その実装クラスにコンパイル時の依存関係を作成すると、その依存関係ツリー全体への壊れないリンクが作成されます。オブジェクトグラフの複雑さは、1桁、おそらく2桁跳ね上がりました。

  • 依存関係ツリーのこれらの項目の多くは透過的ですが、それらの多くはインスタンス化する必要もあります。おそらく、PaypalCreditCardProcessorをインスタンス化することはできません。

  • インスタンス化に加えて、各オブジェクトには構成から適用されるプロパティが必要です。

インターフェースへの依存関係のみがあり、外部ファクトリーが依存関係を構築および注入できるようにする場合、それらはPaypal依存関係ツリー全体を切り捨て、コードの複雑さはインターフェースで止まります。

構成で実装クラスを指定すること(つまり、コンパイル時ではなく実行時)や、環境(テスト、統合、本番など)によって異なる動的依存関係仕様を持つことなど、他の利点もあります。

たとえば、PayPalProcessorに3つの依存オブジェクトがあり、それらの依存関係のそれぞれにさらに2つあるとします。そして、それらすべてのオブジェクトは、構成からプロパティを取得する必要があります。コードは現状のまま構築し、構成などからプロパティを設定するなどの責任を負います。DIフレームワークが処理するすべての問題。

最初は、DIフレームワークを使用して何から自分を保護しているのかは明らかではないように思われるかもしれませんが、時間がたつにつれ、次第に明らかになって痛々しいほど明白になります。 (私は難しい方法でそれを試みた経験から話します笑)

...

実際には、非常に小さなプログラムであっても、最終的にはDIスタイルで記述し、クラスを実装/ファクトリペアに分割します。つまり、SpringのようなDIフレームワークを使用していない場合は、単純なファクトリー・クラスをいくつか一緒に投げるだけです。

これにより、懸念が分離され、私のクラスはそれを実行できるようになり、ファクトリークラスは、ものの構築と構成を担当します。

必須のアプローチではありませんが、FWIW

...

より一般的には、DI /インターフェイスパターンは、次の2つのことを行うことでコードの複雑さを軽減します。

  • 下流の依存関係をインターフェースに抽象化

  • 上流の依存関係をコードから何らかのコンテナに「持ち上げる」

その上、オブジェクトのインスタンス化と構成はかなりおなじみのタスクであるため、DIフレームワークは、標準化された表記法とリフレクションなどのトリックを使用することで、多くの規模の経済を実現できます。クラスに関する同じ懸念を分散させると、思ったよりもはるかに多くの混乱が生じます。

12
Rob
  1. あなたがプールの浅い端で泳ぐとき、すべてが「簡単で便利」です。十数個のオブジェクトを通過すると、もはや便利ではありません。
  2. あなたの例では、あなたはあなたの請求プロセスを永久にそして1日をPaypalに結びつけました。別のクレジットカードプロセッサを使用するとしますか?ネットワークに制約のある専用クレジットカードプロセッサを作成するとしますか?または、クレジットカード番号の処理をテストする必要がありますか?移植性のないコードを作成しました:「1回記述して、1回だけ使用してください。これは、それが設計された特定のオブジェクトグラフに依存するためです。」

プロセスの早い段階でオブジェクトグラフをバインドする(つまり、オブジェクトグラフをコードにハードワイヤリングする)には、コントラクトと実装の両方が存在する必要があります。他の誰か(多分あなたでも)が少し異なる目的でそのコードを使用したい場合は、オブジェクトグラフ全体を再計算して再実装する必要があります。

DIフレームワークを使用すると、一連のコンポーネントを取得して、実行時にそれらをワイヤリングできます。これにより、システムは「モジュラー」になり、相互の実装ではなく相互のインターフェースに対して機能する多数のモジュールで構成されます。

4
BobDalgleish

私はGoogle Guiceを使用していませんが、.Netの古いレガシーN層アプリケーションを、依存性注入に依存して物事を分離するOnion LayerなどのIoCアーキテクチャに移行するのにかなりの時間を費やしました。

依存性注入の理由

Dependency Injectionの目的は実際にはテスト容易性のためではなく、実際には密結合アプリケーションを使用して、可能な限り結合を緩めることです。 (適切な単体テストにコードを適合させるのがはるかに簡単になるという望ましい副産物があります)

なぜカップリングを心配する必要があるのですか?

結合または緊密な依存関係は非常に危険な場合があります。 (特にコンパイルされた言語で)これらの場合、アプリケーション全体を効果的にオフラインにする問題がある非常にまれに使用されるライブラリー、dllなどが存在する可能性があります。 (重要でない部分に問題があるため、アプリケーション全体が停止します...これは悪いです...本当に悪いです)物事を切り離すと、実際にアプリケーションをセットアップして、たとえDLLであっても実行できるようにしますライブラリーがまったくありません!確かに、そのライブラリまたはDLLを必要とする1つの部分は機能しませんが、アプリケーションの残りの部分はできる限り満足して動作します。

適切なテストのために依存性注入が必要な理由

本当に疎結合のコードが必要なだけで、依存性注入はそれを可能にします。 IoCがなくても疎結合できますが、通常は作業量が多く、適応性が低くなります(きっと誰かが例外を持っていると思います)

あなたが与えた場合、私は依存性注入をセットアップする方がはるかに簡単だと思うので、このテストの一部として数えたくないコードをモックすることができます。メソッドに「リポジトリを呼び出すように言ったのはわかっていますが、代わりにここに、データが「返すべき」winks」であることを伝えます。データを実際に取得するのではなく、そのデータを使用する部分のみをテストしていることを知っているので、変更はありません。

基本的にテストする場合は、最初から最後まで機能の一部をテストする統合(機能)テストと、コードの各部分を(通常はメソッドまたは関数レベルで)個別にテストする完全な単体テストが必要です。

機能していないコードの正確な部分を知りたくない場合は、機能全体が機能していることを確認したいという考えです。

これ[〜#〜] [〜#〜]は依存性注入なしで実行できますが、通常、プロジェクトが成長するにつれて、ますます面倒になりますそのため、依存性注入はありません。 (常にプロジェクトが成長することを前提としています!プロジェクトが急速に立ち上がっており、事態がすでに始まった後に深刻なリファクタリングとリエンジニアリングを必要とするよりも、不必要なスキルを練習する方が良いでしょう。)

1
RualStorge

私が言及するように 別の答えで 、ここでの問題は、クラスAいくつかのクラスBハードコーディングなしwhichクラスBは、Aのソースコードに使用されます。これは、 JavaおよびC#です。クラスをインポートする唯一の方法は、グローバルに一意の名前でクラスを参照することだからです。

インターフェイスを使用すると、ハードコードされたクラスの依存関係を回避できますが、それでもインターフェイスのインスタンスを入手する必要があり、コンストラクターを呼び出せないか、正方形1に戻ります。そうでなければ、依存関係を作成して、その責任を他の誰かに押しやることができます。そして、その依存関係は同じことをしています。したがって、今every必要なときにクラスのインスタンスが必要になり、依存関係ツリー全体を手動で構築することになりますが、クラスAがBに直接依存している場合new A()を呼び出し、そのコンストラクターにnew B()を呼び出させることもできます。

依存関係注入フレームワークは、クラス間のマッピングを指定し、依存関係ツリーを構築することで、これを回避しようとします。問題は、マッピングを台無しにすると、最上位の概念としてマッピングモジュールをサポートする言語のように、コンパイル時にではなく、実行時に判明することです。

0
Doval

これはここでは大きな誤解だと思います。

Guiceは依存関係の注入frameworkです。 DI 自動になります。あなたが引用した抜粋で彼らが作った点は、Guiceがあなたの例で示した「テスト可能なコンストラクタ」を手動で作成する必要を取り除くことができるということです。依存性注入自体とはまったく関係ありません。

このコンストラクタ:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

すでに依存性注入を使用しています。基本的に、DIの使用は簡単だとおっしゃっていました。

Guiceが解決する問題は、そのコンストラクターを使用するには、オブジェクトグラフコンストラクターコードどこかが必要であり、既にインスタンス化されているオブジェクトをそのコンストラクターの引数として手動で渡す必要があるということです。 Guiceでは、CreditCardProcessorおよびTransactionLogインターフェースに対応する実際の実装クラスを構成できる単一の場所を設定できます。その構成の後、Guiceを使用してBillingServiceを作成するたびに、それらのクラスはコンストラクターに自動的に渡されます。

これが、依存性注入frameworkが行うことです。しかし、あなたが提示したコンストラクター自体は、すでに依存性注入原理の実装です。 IoCコンテナーとDIフレームワークは、対応する原則を自動化する手段ですが、すべてを手作業で行うことを妨げるものは何もありませんでした。

0
hijarian