この記事では、単純な言語での依存性注入の基本(Eng。Dependency Injection、DI )、およびこのアプローチを使用する理由について説明します。 この記事は、依存性注入とは何かを知らない人、またはこの手法を使用する必要性を疑う人を対象としています。 それでは始めましょう。
中毒とは何ですか?
最初に例を見てみましょう。 以下に示すように、 ClassA
、 ClassB
、およびClassC
があります。
class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { }
ClassA
クラスにはClassB
クラスのインスタンスが含まれていることがわかります。したがって、 ClassA
クラスはClassB
クラスに依存していると言えClassB
。 なんで? ClassA
クラスはClassB
クラスを正しく動作させる必要があるためClassB
。 ClassB
クラスはClassA
クラスの依存関係であるとも言えます。
続行する前に、アプリケーションですべての作業を行うために1つのクラスが必要なわけではないため、この関係が良好であることを明確にしたいと思います。 ロジックを異なるクラスに分割する必要があり、それぞれが特定の機能を担当します。 この場合、クラスは効果的に相互作用できます。
依存関係を扱う方法は?
依存性注入タスクを完了するために使用される3つのメソッドを見てみましょう。
最初の方法:依存クラスで依存関係を作成する
簡単に言えば、必要なときにいつでもオブジェクトを作成できます。 次の例を見てください。
class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } }
とても簡単です! 必要なときにクラスを作成します。
メリット
- 簡単でシンプルです。
- 依存クラス(この例では
ClassA
)は、依存関係を作成する方法とタイミングを完全に制御します。
短所
-
ClassA
とClassB
互いに密接に関連しています。 したがって、ClassA
を使用する必要があるときはいつでも、ClassB
使用を強制され、ClassB
を他のものに置き換えることはClassB
になります 。 -
ClassB
クラスの初期化に変更がある場合、ClassA
クラス(およびClassB
依存する他のすべてのクラス)内のコードを調整する必要があります。 これにより、依存関係を変更するプロセスが複雑になります。 -
ClassA
はテストできません。 クラスをテストする必要があり、これがソフトウェア開発の最も重要な側面の1つである場合、各クラスのユニットテストを個別に実施する必要があります。 つまり、ClassA
クラスの正しい操作をClassA
に検証し、検証するためのいくつかの単体テストを作成する場合は、例に示すように、興味のない場合でもClassB
クラスのインスタンスも作成します。 テスト中にエラーが発生した場合、ClassA
またはClassB
どこにエラーがあるかを理解できません。 結局のところ、ClassA
正常に動作しているときに、ClassB
のコードの一部がエラーにつながる可能性があります。 つまり、モジュール(クラス)を互いに分離できないため、ユニットテストはできません。 -
ClassA
は、依存関係を注入できるように構成する必要があります。 この例では、彼はClassC
を作成し、それを使用してClassB
を作成する方法を知っている必要がありClassB
。 彼がそれについて何も知らなければもっといいでしょう。 なんで? 単一責任の原則のため。
各クラスはその仕事のみを行う必要があります。
したがって、クラスが自分のタスク以外のことを担当することは望ましくありません。 この場合の依存関係の実装は、それらに設定する追加のタスクです。
2番目の方法:カスタムクラスを介して依存関係を注入する
そのため、依存クラス内に依存関係を注入するのは得策ではないことを理解して、別の方法を調べてみましょう。 ここで、依存クラスはコンストラクタ内で必要なすべての依存関係を定義し、ユーザークラスがそれらを提供できるようにします。 これは私たちの問題の解決策ですか? 少し後で確認します。
以下のサンプルコードをご覧ください。
class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium -
これで、 ClassA
はコンストラクター内のすべての依存関係を取得し、何も初期化せずにClassB
クラスのメソッドを単純に呼び出すことができます。
メリット
-
ClassA
とClassB
疎結合になり、ClassA
内のコードを壊すことなくClassB
を置き換えることができClassB
。 たとえば、ClassB
を渡す代わりに、ClassB
のサブクラスであるAssumeClassB
を渡すことができ、プログラムは適切に動作します。 -
ClassA
をテストできるようになりました。 単体テストを作成する場合、独自のバージョンのClassB
(テストオブジェクト)を作成し、それをClassA
渡すことができます。 テストのパス中にエラーが発生した場合、これがClassA
エラーであることは間違いありません。 -
ClassB
依存関係の処理から解放され、タスクに集中できます。
欠点
- このメソッドはチェーンメカニズムに似ており、ある時点でチェーンを中断する必要があります。 言い換えると、
ClassA
クラスのユーザーはClassB
の初期化に関するすべてを知っている必要があり、そのためにはClassC
などの初期化に関する知識が必要ClassB
。 したがって、これらのクラスのコンストラクターを変更すると、ClassA
が複数のユーザーを持つことができることは言うまでもなく、呼び出し元のクラスが変更される可能性があるため、オブジェクトを作成するロジックが繰り返されます。 - 依存関係は明確で理解しやすいという事実にもかかわらず、ユーザーコードは重要で管理が困難です。 したがって、すべてがそれほど単純ではありません。 さらに、コードは単一の責任の原則に違反しています。コードは、その作業だけでなく、依存クラスの依存関係の実装にも責任があるためです。
2番目の方法は明らかに1番目の方法よりもうまく機能しますが、まだ欠点があります。 より適切なソリューションを見つけることは可能ですか? 3番目の方法を検討する前に、まず依存性注入の概念について説明しましょう。
依存性注入とは何ですか?
依存性注入は、依存クラスが何もする必要がない場合に、依存クラス外の依存関係を処理する方法です。
この定義に基づいて、最初のソリューションは明らかに依存性注入のアイデアを使用しません。2番目の方法は、依存クラスが依存関係を提供するために何もしないということです。 しかし、2番目の解決策はまだ悪いと思います。 なぜ?!
依存性注入の定義は、依存性のある作業が行われる場所については何も述べていないので(依存クラスの外部を除く)、開発者は依存性注入の適切な場所を選択する必要があります。 2番目の例からわかるように、ユーザークラスは適切な場所ではありません。
より良い方法は? 依存関係を処理する3番目の方法を見てみましょう。
第三の方法:私たちの代わりに他の誰かに依存関係を処理させます
最初のアプローチでは、依存クラスが独自の依存関係を取得する責任があり、2番目のアプローチでは、依存クラスの処理を依存クラスからユーザークラスに移動しました。 依存関係を処理できる誰かがいて、その結果、依存クラスもユーザークラスも仕事をしないと想像してみましょう。 このメソッドを使用すると、アプリケーションの依存関係を直接操作できます。
依存性注入の「クリーンな」実装(個人的な意見)
依存関係を処理する責任はサードパーティにあるため、アプリケーションのどの部分も依存関係を操作しません。
依存性注入は、テクノロジー、フレームワーク、ライブラリーなどではありません。 これは単なるアイデアです。 考え方は、依存クラスの外側の依存関係(できれば特別に割り当てられた部分)を使用することです。 ライブラリやフレームワークを使用せずにこのアイデアを適用できます。 ただし、作業を簡素化し、テンプレートコードを記述する必要がないため、通常、依存関係を実装するためのフレームワークを使用します。
依存性注入フレームワークには、2つの固有の特性があります。 他の追加機能が利用できる場合がありますが、これらの2つの機能は常に存在します。
まず、これらのフレームワークは、実装する必要があるフィールド(オブジェクト)を決定する方法を提供します。 一部のフレームワークは、 @Inject
注釈を使用してフィールドまたはコンストラクターに注釈を付けることでこれを行いますが、他のメソッドもあります。 たとえば、 KoinはKotlinの組み込み言語機能を使用して実装を決定します。 Inject
は、依存関係がDIフレームワークによって処理される必要があることを意味します。 コードは次のようになります。
class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } }
次に、フレームワークを使用すると、各依存関係を提供する方法を決定できます。これは、個別のファイルで行われます。 おおよそ次のようになります(これは単なる例であり、フレームワークによって異なる場合があることに注意してください)。
class OurThirdPartyGuy { fun provideClassC(){ return ClassC() //just creating an instance of the object and return it. } fun provideClassB(classC: ClassC){ return ClassB(classC) } fun provideClassA(classB: ClassB){ return ClassA(classB) } }
したがって、ご覧のとおり、各関数は1つの依存関係を処理します。 そのため、アプリケーションのどこかでClassA
を使用する必要がある場合、次のことが発生します:DIフレームワークは、 ClassC
を呼び出してそれをprovideClassB
に渡し、 ClassB
インスタンスを受け取ることでClassC
クラスのインスタンスを1つ作成します。 これはほとんど魔法です。 次に、3番目の方法の利点と利点を調べてみましょう。
メリット
- すべてが可能な限りシンプルです。 依存クラスと依存関係を提供するクラスの両方が明確でシンプルです。
- クラスは疎結合であり、他のクラスに簡単に置き換えることができます。
ClassC
、AssumeClassC
のサブクラスであるClassC
に置き換えたいとします。 これを行うには、次のようにプロバイダーコードを変更するだけで、ClassC
が使用されている場合はClassC
、新しいバージョンが自動的に使用されます。
fun provideClassC(){ return AssumeClassC() }
アプリケーション内のコードは変更されず、プロバイダーメソッドのみが変更されることに注意してください。 さらにシンプルで柔軟性のあるものはないようです。
- 信じられないほどのテスト容易性。 テスト中に依存関係をテストバージョンに簡単に置き換えることができます。 実際、依存関係の注入は、テストに関しては主要なヘルパーです。
- コード構造の改善、として アプリケーションには、依存関係を処理するための別の場所があります。 その結果、アプリケーションの残りの部分は、その機能を実行することに専念でき、依存関係と重複することはありません。
短所
- DIフレームワークには一定のエントリーしきい値があるため、プロジェクトチームはそれを効果的に使用する前に時間をかけて調査する必要があります。
おわりに
- DIを使用しない依存関係の処理は可能ですが、アプリケーションがクラッシュする可能性があります。
- DIは単なる効果的なアイデアであり、これにより、依存クラスの外側の依存関係を処理することができます。
- アプリケーションの特定の部分でDIを使用するのが最も効果的です。 多くのフレームワークがこれに貢献しています。
- フレームワークとライブラリはDIには必要ありませんが、多くの場合に役立ちます。
この記事では、依存性注入の概念を扱う基本を説明し、このアイデアを使用する理由もリストしました。 独自のアプリケーションでDIを使用する方法の詳細については、さらに多くのリソースを探索できます。 たとえば、 Androidプロフェッショナルコースの高度な部分の別のセクションは、このトピック専用です。