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


13

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

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

手作業でオブジェクトグラフを作成するのは手間がかかり(...)、テストが困難になります。

しかし、私はこの議論を買いません。依存性注入フレームワークがなくても、インスタンス化が簡単で、テストが便利なクラスを作成できます。たとえば、Guiceの動機付けページの例を次のように書き換えることができます。

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つではありませんか?


1
多くの場合無視される依存性注入の別の利点は、どのオブジェクトが依存しているかを知るように強制することだと思います。オブジェクトには魔法のように何も表示されません。明示的にマークされた属性を注入するか、コンストラクターを介して直接注入します。コードがよりテスト可能になる方法は言うまでもありません。
ベンジャミングリュンバウム14

私は唯一の「オブジェクトのインスタンス化は、依存性注入なしでは困難である」という主張を理解することに興味がある-私は依存性注入の他の利点を探していないよという注意
oberlies

この答えは、オブジェクトグラフに何らかのコンテナを必要とする理由の非常に良い例を提供するように思えます(要するに、3つのオブジェクトのみを使用して、十分に大きく考えていません)。
イズカタ14

@Izkataいいえ、これはサイズの問題ではありません。説明したアプローチでは、その投稿のコードはになりますnew ShippingService()
服従14

@oberlies Still is; あなたはその後、クラスがそのShippingServiceのために使用されているかを把握する9クラス定義ではなく、一つの場所で掘りに行く必要があるだろう
Izkata

回答:


12

依存性注入を行う最良の方法については、古くからの継続的な議論があります。

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

  • しかし、その後、多くの人々は、コンストラクターパラメーターを使用して依存関係を注入することが正しい方法であると主張しました。

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

したがって、最初のコンストラクターは、依存性注入の2番目のアプローチと一貫しています。これにより、テスト用にモックを注入するなどの便利なことができます。

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

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

  • 依存関係ツリー内のこれらのアイテムの多くは透過的ですが、それらの多くもインスタンス化する必要があります。可能性は、単にインスタンス化することはできませんPaypalCreditCardProcessor

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

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

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

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

DIフレームワークを使用することで何を防いでいるのかは、最初は明らかではないように思えるかもしれませんが、時間の経過とともに積み重なっていきます。(笑私はそれを苦労してやろうとした経験から話しています)

...

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

それは懸念を分離するので、私のクラスはそれを行うことができ、ファクトリクラスはものの構築と構成の責任を引き受けます。

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

...

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

  • ダウンストリームの依存関係をインターフェイスに抽象化する

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

その上、オブジェクトのインスタンス化と構成はかなり馴染みのあるタスクであるため、DIフレームワークは標準化された表記法とリフレクションのようなトリックを使用することで、多くのスケールメリットを達成できます。クラスに関するこれらの同じ懸念を散らすことは、最終的には考えられるよりもはるかに多くの混乱を追加することになります。


コードはそのすべてを構築する責任を負います -クラスは、コラボレーターの実装が何であるかを知る責任を負います。これらのコラボレーターが順番に何を必要としているかを知る必要はありません。したがって、これはそれほど多くありません。
服従14

1
しかし、PayPalProcessorコンストラクターに2つの引数がある場合、それらはどこから来るのでしょうか?
ロブ14

そうではありません。BillingServiceと同様に、PayPalProcessorには引数がゼロのコンストラクターがあり、必要なコラボレーターを作成します。
服従14

ダウンストリームの依存関係をインターフェイスに抽象化する -サンプルコードもこれを実行します。プロセッサフ​​ィールドのタイプはCreditCardProcessorであり、実装タイプではありません。
服従14

2
@oberlies:コード例は、コンストラクター引数スタイルと引数ゼロの構築可能なスタイルという2つのスタイルにまたがっています。誤解は、ゼロ引数の構築が常に可能であるとは限らないということです。DIまたはFactoryの利点は、引数がゼロのコンストラクターのように見えるオブジェクトを何らかの方法で構築できることです。
rwong 14

4
  1. プールの浅い端で泳ぐと、すべてが「簡単で便利」になります。1ダースほどのオブジェクトを取得すると、もはや便利ではなくなります。
  2. この例では、請求プロセスを永久に1日、PayPalにバインドしています。別のクレジットカードプロセッサを使用するとしますか?ネットワークに制約のある特殊なクレジットカードプロセッサを作成するとしますか?または、クレジットカード番号の処理をテストする必要がありますか?移植性のないコードを作成しました:「一度書くだけで、それが設計された特定のオブジェクトグラフに依存するため、一度だけ使用する」。

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

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


あなたの議論に従えば、DIの理由は「オブジェクトグラフを手動で作成するのは簡単ですが、維持するのが難しいコードにつながる」ことでした。ただし、「手動でオブジェクトグラフを作成するのは難しい」という主張であり、私はその主張をサポートする引数のみを探しています。したがって、あなたの投稿は私の質問に答えません。
oberlies

1
あなたが質問を削減する場合、私は推測する「作成オブジェクトグラフを...」、それは簡単です。私のポイントは、DIは1つのオブジェクトグラフだけを扱うことはないという問題に対処するということです。あなたは彼らの家族を扱っています。
BobDalgleish 14

実際、コラボレーターが共有されている場合、 1つのオブジェクトグラフを作成するだけでは既に注意が必要です。
服従14

4

「自分のコラボレーターをインスタンス化する」アプローチは、依存関係ツリーではうまくいくかもしれませんが、一般的な有向非循環グラフ(DAG)である依存関係グラフでは確かにうまくいきません。依存関係DAGでは、複数のノードが同じノードを指すことができます。つまり、2つのオブジェクトが共同作業者として同じオブジェクトを使用します。実際、このケースは、質問で説明されているアプローチでは構築できません。

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


1
しかし、グラフ理論の意味では、依存ツリーツリーでなければなりません。それ以外の場合は、サイクルがありますが、これは単に不満です。
Xion 14

1
@Xion依存グラフは、有向非巡回グラフである必要があります。ツリーのように2つのノード間に正確に1つのパスしかないという要件はありません。
服従14

@Xion:必ずしもではありません。UnitOfWork単一DbContextおよび複数のリポジトリがあるa を考慮してください。これらのリポジトリはすべて同じDbContextオブジェクトを使用する必要があります。OPが提案する「自己インスタンス化」により、それは不可能になります。
平らな

1

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

なぜ依存性注入ですか?

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

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

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

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

本当に疎結合のコードが必要なだけで、Dependency Injectionはそれを可能にします。IoCを使用せずに大まかに結合することはできますが、通常は作業が多くなり、適応性が低下します(そこには例外があります)

あなたが与えた場合、このテストの一部としてカウントすることに興味のないコードをモックできるように、依存性注入をセットアップする方がはるかに簡単だと思います。「リポジトリを呼び出すように言ったが、代わりにウィンクを返す必要があるデータ」という方法を教えてください。そのデータは変更されないため、そのデータを使用する部分だけをテストしていることを知っていますデータの実際の取得。

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

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

これは、CAN依存性の注入なしで行われますが、通常はあなたのプロジェクトとして、それは場所に依存性注入せずにそうするように、ますます厄介になり成長すること。(常にプロジェクトが成長することを前提としています!プロジェクトが急速に立ち上がっており、物事がすでに始まっている後に深刻なリファクタリングとリエンジニアリングを必要とするよりも、有用なスキルの不必要な練習をする方が良いです。)


1
依存グラフのすべてではないが大部分を使用してテストを記述することは、実際にはDIの良い議論です。
服従14

1
また、DIを使用すると、プログラムでコードのチャンク全体を簡単に交換できることに注意してください。システムが完全に異なるクラスのインターフェイスをクリアして再注入し、リモートサービスによる停止やパフォーマンスの問題に対応する場合を見てきました。それを処理するより良い方法があったかもしれませんが、驚くほどうまく機能しました。
RualStorge

0

私が言及したよう別の答えに、ここでの問題は、クラスが欲しいということであるAに依存するいくつかのクラスBのハードコーディングすることなく、どのクラスB唯一の方法は、クラスをインポートするためA.これのソースコードに使用されているが、JavaやC#には不可能ですグローバルに一意の名前でそれを参照することです。

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

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


0

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

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

このコンストラクタ:

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

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

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

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

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