伝統的な景色 制御の反転(IoC)は、サービスロケーターと依存性注入(DI)パターンという2つの異なるアプローチの間に明確な線を引くようです。
私が知っている事実上すべてのプロジェクトには、DIフレームワークが含まれています。人々は、ボイラープレートコードを最小限に抑えるか、まったく使用せずに、クライアントとその依存関係の間の疎結合を促進するため(通常はコンストラクターインジェクションを通じて)、それらに惹かれます。これは迅速な開発には最適ですが、コードのトレースとデバッグが困難になる可能性があることに気付く人もいます。 「舞台裏の魔法」は通常、反射によって実現されます。これにより、一連の新しい問題が発生する可能性があります。
この記事では、Java8以降およびKotlinコードベースに適した代替パターンについて説明します。外部ツールを必要とせずに、サービスロケーターと同じくらい簡単でありながら、DIフレームワークの利点のほとんどを保持します。
次の例では、さまざまなソースを使用してコンテンツを取得できるTV実装をモデル化します。さまざまなソース(地上波、ケーブル、衛星など)から信号を受信できるデバイスを構築する必要があります。次のクラス階層を構築します。
それでは、Springなどのフレームワークがすべてを配線している従来のDI実装から始めましょう。
public class TV { private final TvSource source; public TV(TvSource source) { this.source = source; } public void turnOn() { System.out.println('Turning on the TV'); this.source.tuneChannel(42); } } public interface TvSource { void tuneChannel(int channel); } public class Terrestrial implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Adjusting dish frequency to channel %d
', channel); } } public class Cable implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Changing digital signal to channel %d
', channel); } }
私たちはいくつかのことに気づきます:
順調なスタートを切っていますが、このためのDIフレームワークを導入するのは少しやり過ぎかもしれません。一部の開発者は、構築の問題(長いスタックトレース、追跡不可能な依存関係)のデバッグに関する問題を報告しています。クライアントはまた、製造時間が予想よりも少し長いことを表明しており、プロファイラーはリフレクティブコールの速度低下を示しています。
別の方法は、ServiceLocatorパターンを適用することです。これは単純で、リフレクションを使用せず、小さなコードベースには十分かもしれません。もう1つの方法は、クラスをそのままにして、それらの周りに依存関係の場所のコードを記述することです。
多くの選択肢を評価した後、プロバイダーインターフェイスの階層として実装することを選択します。各依存関係には、クラスの依存関係を特定し、挿入されたインスタンスを構築する唯一の責任を持つプロバイダーが関連付けられます。また、プロバイダーを使いやすい内部インターフェイスにします。各プロバイダーが他のプロバイダーと混合されて依存関係を特定するため、これをMixinインジェクションと呼びます。
私がこの構造に落ち着いた理由の詳細は、詳細と理論的根拠で詳しく説明されていますが、ここに短いバージョンがあります。
次の図は、依存関係とプロバイダーがどのように相互作用するかを示しており、実装を以下に示します。また、依存関係を作成してTVオブジェクトを作成する方法を示すmainメソッドを追加します。この例のより長いバージョンもこれにあります GitHub 。
public interface TvSource { void tuneChannel(int channel); interface Provider { TvSource tvSource(); } } public class TV { private final TvSource source; public TV(TvSource source) { this.source = source; } public void turnOn() { System.out.println('Turning on the TV'); this.source.tuneChannel(42); } interface Provider extends TvSource.Provider { default TV tv() { return new TV(tvSource()); } } } public class Terrestrial implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Adjusting dish frequency to channel %d
', channel); } interface Provider extends TvSource.Provider { @Override default TvSource tvSource() { return new Terrestrial(); } } } public class Cable implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf('Changing digital signal to channel %d
', channel); } interface Provider extends TvSource.Provider { @Override default TvSource tvSource() { return new Cable(); } } } // Here compose the code above to instantiate a TV with a Cable TvSource public class Main { public static void main(String[] args) { new MainContext().tv().turnOn(); } static class MainContext implements TV.Provider, Cable.Provider { } }
この例に関するいくつかの注意事項:
動作中のパターンとその背後にあるいくつかの理由を見てきました。あなたは今までにそれを使うべきだと確信していないかもしれません、そしてあなたは正しいでしょう。正確には特効薬ではありません。個人的には、ほとんどの面でサービスロケーターパターンよりも優れていると思います。ただし、DIフレームワークと比較した場合、ボイラープレートコードを追加するオーバーヘッドを利点が上回るかどうかを評価する必要があります。
プロバイダーが別のプロバイダーを拡張すると、依存関係がバインドされます。これは、無効なコンテキストの作成を防ぐ静的検証の基本的な基盤を提供します。
サービスロケーターパターンの主な問題点の1つは、ジェネリックGetService()
を呼び出す必要があることです。どういうわけかあなたの依存関係を解決する方法。コンパイル時に、依存関係がロケーターに登録されるという保証はなく、プログラムは実行時に失敗する可能性があります。
DIパターンもこれに対応していません。依存関係の解決は通常、ユーザーからほとんど隠されている外部ツールによるリフレクションによって行われます。依存関係が満たされない場合、実行時にも失敗します。などのツール IntelliJのCDI (有料版でのみ利用可能)ある程度の静的検証を提供しますが、 短剣 注釈プリプロセッサを使用すると、この問題に意図的に取り組んでいるようです。
これは必須ではありませんが、開発者コミュニティによって確実に望まれています。一方では、コンストラクターを見るだけで、クラスの依存関係をすぐに確認できます。一方、それは可能にします ユニットテストの種類 多くの人がそれを順守します。それは、依存関係のモックを使用してテスト対象のサブジェクトを構築することです。
これは、他のパターンがサポートされていないということではありません。実際、Mixinインジェクションは、サブジェクトのプロバイダーを拡張するコンテキストクラスを実装するだけでよいため、テスト用の複雑な依存関係グラフの作成を簡素化することに気付くかもしれません。 MainContext
上記は、すべてのインターフェースにデフォルトの実装があるため、空の実装を持つことができる完璧な例です。依存関係を置き換えるには、プロバイダーメソッドをオーバーライドするだけで済みます。
TVクラスの次のテストを見てみましょう。 TVをインスタンス化する必要がありますが、クラスコンストラクターを呼び出す代わりに、TV.Providerインターフェイスを使用しています。 TvSource.Providerにはデフォルトの実装がないため、自分で作成する必要があります。
public class TVTest { @Test public void testWithProvider() { TvSource source = Mockito.mock(TvSource.class); TV.Provider provider = () -> source; // lambdas FTW provider.tv().turnOn(); Mockito.verify(source, times(1)).tuneChannel(42); } }
次に、TVクラスに別の依存関係を追加しましょう。 CathodeRayTube依存関係は、テレビ画面に画像を表示する魔法の働きをします。将来的にLCDまたはLEDに切り替えたい可能性があるため、TVの実装から切り離されています。
public class TV { public TV(TvSource source, CathodeRayTube cathodeRayTube) { ... } public interface Provider extends TvSource.Provider, CathodeRayTube.Provider { default TV tv() { return new TV(tvSource(), cathodeRayTube()); } } } public class CathodeRayTube { public void beam() { System.out.println('Beaming electrons to produce the TV image'); } public interface Provider { default CathodeRayTube cathodeRayTube() { return new CathodeRayTube(); } } }
これを行うと、先ほど作成したテストが引き続きコンパイルされ、期待どおりに合格することがわかります。 TVに新しい依存関係を追加しましたが、デフォルトの実装も提供しました。つまり、実際の実装を使用するだけの場合は、モックを作成する必要はありません。テストでは、必要なレベルのモック粒度で複雑なオブジェクトを作成できます。
これは、複雑なクラス階層(たとえば、データベースアクセス層のみ)で特定のものをモックしたい場合に便利です。このパターンにより、種類を簡単に設定できます 社交的なテスト これは、単独のテストよりも好まれる場合があります。
好みに関係なく、それぞれの状況でのニーズにより適した任意の形式のテストに頼ることができると確信できます。
ご覧のとおり、外部コンポーネントへの参照や言及はありません。これは、サイズやセキュリティ上の制約がある多くのプロジェクトにとって重要です。フレームワークは特定のDIフレームワークにコミットする必要がないため、相互運用性にも役立ちます。 Javaでは、次のような取り組みが行われています。 JavaStandard用のJSR-330依存性注入 互換性の問題を軽減します。
サービスロケーターの実装は通常、リフレクションに依存しませんが、DIの実装はリフレクションに依存します(Dagger 2の注目すべき例外を除く)。これには、フレームワークがモジュールをスキャンし、依存関係グラフを解決し、オブジェクトを反射的に構築する必要があるため、アプリケーションの起動が遅くなるという主な欠点があります。
Mixinインジェクションでは、サービスロケーターパターンの登録手順と同様に、サービスをインスタンス化するためのコードを記述する必要があります。この少し余分な作業により、リフレクティブコールが完全に削除され、コードがより高速で簡単になります。
最近私の注意を引き、反省を避けることで恩恵を受ける2つのプロジェクトは Graalの基板VM そして Kotlin / Native 。どちらもネイティブバイトコードにコンパイルされます。これには、コンパイラが、実行するリフレクティブ呼び出しを事前に認識している必要があります。 Graalの場合、それはで指定されます 書きにくいJSONファイル 、静的にチェックすることはできず、お気に入りのツールを使用して簡単にリファクタリングすることはできません。そもそもリフレクションを回避するためにMixinInjectionを使用することは、ネイティブコンパイルの利点を得る優れた方法です。
必要なインターフェースを実装および拡張することにより、依存関係グラフを一度に1つずつ作成します。各プロバイダーは具体的な実装の隣にあり、プログラムに順序とロジックをもたらします。この種のレイヤリングは、以前にMixinパターンまたはCakeパターンを使用したことがある場合はおなじみです。
この時点で、MainContextクラスについて説明する価値があるかもしれません。これは依存関係グラフのルートであり、全体像を把握しています。このクラスにはすべてのプロバイダーインターフェイスが含まれ、静的チェックを有効にするための鍵となります。例に戻ってCable.Providerを実装リストから削除すると、次のことがはっきりとわかります。
s corp ccorpとllcの違い
static class MainContext implements TV.Provider { } // ^^^ // MainContext is not abstract and does not override abstract method tvSource() in TvSource.Provider
ここで起こったことは、アプリが使用する具体的なTvSourceを指定しておらず、コンパイラがエラーをキャッチしたことです。サービスロケーターとリフレクションベースのDIを使用すると、すべての単体テストに合格した場合でも、実行時にプログラムがクラッシュするまで、このエラーに気付かなかった可能性があります。これらと私たちが示した他の利点は、パターンを機能させるために必要な定型文を書くことの欠点を上回っていると思います。
CathodeRayTubeの例に戻り、循環依存関係を追加しましょう。 TVインスタンスを注入したいので、TV.Providerを拡張するとします。
public class CathodeRayTube { public interface Provider extends TV.Provider { // ^^^ // cyclic inheritance involving CathodeRayTube.Provider default CathodeRayTube cathodeRayTube() { return new CathodeRayTube(); } } }
コンパイラは循環継承を許可しておらず、この種の関係を定義することはできません。これが発生すると、ほとんどのフレームワークは実行時に失敗し、開発者はプログラムを実行するためだけにフレームワークを回避する傾向があります。このアンチパターンは現実の世界で見られますが、通常は悪いデザインの兆候です。コードのコンパイルに失敗した場合は、変更するのに手遅れになる前に、より良い解決策を探すことをお勧めします。
DIよりもSLを支持する議論の1つは、デバッグが簡単で簡単であるということです。例から、依存関係のインスタンス化はプロバイダーメソッド呼び出しのチェーンにすぎないことが明らかです。依存関係のソースをさかのぼるのは、メソッド呼び出しにステップインして、最終的にどこに到達するかを確認するのと同じくらい簡単です。プロバイダーから直接、依存関係がインスタンス化される場所を正確にナビゲートできるため、デバッグは両方の方法よりも簡単です。
注意深い読者は、この実装がサービス寿命の問題に対処していないことに気付いたかもしれません。プロバイダーメソッドへのすべての呼び出しは、新しいオブジェクトをインスタンス化し、これを次のようにします。 Springのプロトタイプスコープ 。
詳細を気にせずにパターンの本質を提示したかっただけなので、これやその他の考慮事項はこの記事の範囲から少し外れています。ただし、製品の完全な使用と実装では、ライフタイムサポートを備えた完全なソリューションを考慮する必要があります。
依存性注入フレームワークに慣れている場合でも、独自のサービスロケーターを作成している場合でも、この代替手段を検討することをお勧めします。今見たミックスインパターンの使用を検討し、コードをより安全で推論しやすくすることができるかどうかを確認してください。
関連: JSのベストプラクティス:TypeScriptと依存性注入を使用してDiscordボットを構築するコードの一部が、そのコードを書いている時点では知られていない別のコンポーネントに動作を委任できる場合の制御の反転について説明します(通常は、よく知られているインターフェイスと拡張ポイントを介して)。
依存性注入は、システムのコンポーネントレベルで制御の反転を実現できるパターンです。クラスは、目標を達成するために必要なコンポーネントのみを宣言し、それらのコンポーネントを検索または作成する方法は宣言しません。