JavaScriptにおける依存性注入
Jerry Ejonavi
2022年11月17日
0 分で読めます制御の反転(IoC)技法を使うと、開発者は従来のプログラミングの流れから抜け出し、コードをより柔軟に、より細かく制御できるようになります。IoCの一種である依存性注入は、オブジェクトの構築と使用を切り分けることを目的としたパターンです。
この記事では、依存性注入とは何か、どのような場合に使うべきか、また、どの人気JavaScriptフレームワークに実装されているかを解説します。
依存性注入とは?
依存性注入とは、オブジェクトや関数が、内部実装の詳細を気にせずに他のオブジェクトや関数(依存関係)を利用するソフトウェア設計パターンです。必要な場所に依存関係を提供する役割は、インジェクター(アセンブラー、プロバイダー、コンテナーとも呼ばれます)が担います。
たとえば、ビデオゲーム機は、互換性のあるディスクやカートリッジがあれば動作します。ディスクやカートリッジには、それぞれ異なるゲームの情報が収録されています。プレイヤーはゲーム機の内部構造を知る必要はなく、通常はゲームをプレイするためにディスクを挿入したり交換したりするだけです。
このようにソフトウェアを設計したりコードを書いたりすると、コンポーネントの削除や置き換え(上の例のように)、ユニットテストの作成、コードベースの定型コードの削減が容易になります。
依存性注入は、特にJava、C#、Pythonなどの言語では一般的に実装・利用されているパターンですが、JavaScriptではあまり見られません。以降では、このパターンのメリットとデメリット、人気のJavaScriptフレームワークでの実装方法、そして依存性注入を念頭に置いたJavaScriptプロジェクトの構築方法を紹介します。
依存性注入が必要な理由
依存性注入が役立つ理由を十分に理解するには、IoC技法がどのような問題を解決するのかを知ることが重要です。たとえば、先ほどのビデオゲーム機の例を考えてみましょう。その機能をコードで表現してみます。この例では、ゲーム機をNoSleepStationと呼び、次のように仮定します。
NoSleepStationは、NoSleepStation向けに設計されたゲームのみをプレイできます。
ゲーム機で使用できる入力ソースは、コンパクトディスクだけです。
この情報をもとに、NoSleepStationは次のように実装できます。
ここでは、ゲーム機の中核となるロジックがGameReaderクラスにあり、依存先としてNSSConsoleがあります。コンソールのplayメソッドは、GameReader インスタンスを使ってゲームを起動します。しかし、柔軟性やテストなど、いくつかの問題が見受けられます。
柔軟性
先ほどのコードには柔軟性がありません。ユーザーが別のゲームをプレイしたい場合、NSSConsoleクラスを変更する必要があります。これは、実際のゲーム機を分解するようなものです。中核となる依存関係であるGameReaderクラスが、NSSConsoleの実装にハードコードされているためです。
依存性注入は、クラスとその依存関係を切り離し、必要なときにだけ依存関係を提供することで、この問題を解決します。先ほどのコード例では、NSSConsoleクラスに必要なのは、GameReaderインスタンスのreadDisc()メソッドだけです。
依存性注入を使うと、先ほどのコードは次のように書き換えられます。
このコードで最も重要な変更点は、NSSConsoleクラスとGameReader クラスが切り離されていることです。NSSConsoleが動作するには引き続きGameReaderインスタンスが必要ですが、それを明示的に作成する必要はありません。GameReaderインスタンスを作成してNSSConsoleに渡す役割は、依存性注入プロバイダーが担います。
大規模なコードベースでは、依存関係の作成と接続を依存性注入プロバイダーが処理するため、定型コードを大幅に削減できます。つまり、特定のクラスが必要とするクラスのインスタンスを作成する必要がありません。依存先がさらに依存関係を持つ場合は、特に役立ちます。
テスト
依存性注入の最も重要なメリットの一つは、ユニットテストにあります。クラスの依存関係を外部プロバイダーに用意してもらうことで、テスト対象ではないオブジェクトの代わりにモックやスタブを使用できます。
依存関係のインターフェース(公開されているメソッドやプロパティ)が変わらなければ、実際のインスタンスの代わりに、同じインターフェースを持つシンプルなオブジェクトを使ってユニットテストを実施できます。
依存性注入の制約
依存性注入にもデメリットがあることに注意が必要です。通常、そのデメリットは、コードの定型部分を減らすという問題に依存性注入がどう取り組むかに関係しています。
依存性注入のメリットを最大限に得るには、特に大規模なコードベースの場合、依存性注入プロバイダーを設定する必要があります。この設定には多くの作業が必要となり、シンプルなプロジェクトでは余計な負担になることもあります。
依存性注入ライブラリやフレームワークを使えば、この問題を解決できます。ただし、設定方法に関して異なる依存性注入フレームワーク間で共通するAPIが少ないため、ほとんどの場合、依存性注入フレームワークを全面的に採用する必要があります。
人気のJavaScriptフレームワークにおける依存性注入
依存性注入は、多くの人気JavaScriptフレームワークに欠かせない機能です。特にAngular()、NestJS、Vue.jsで利用されています。
Angular
Angularは、Webアプリケーションの構築、テスト、保守に必要なツールがそろったクライアントサイドのWeb開発フレームワークです。依存性注入がフレームワークに組み込まれているため、設定は不要で、Angularを使った開発に推奨される方法です。
以下は、Angularで依存性注入がどのように機能するかを示すコード例です(Angularのドキュメントから引用)。
Angularでは、クラスに@Injectableデコレーターを付けることで、そのクラスを注入可能にします。注入可能なクラスを依存先で利用できるようにする方法は、次の3つです。
コンポーネントレベルで
@Componentデコレーターのprovidersフィールドを使用するNgModuleレベルで
@NgModuleデコレーターのprovidersフィールドを使用するアプリケーションのルートレベルで
@InjectableデコレーターにprovidedIn: 'root'を追加する(先ほどの例を参照)
Angularで依存関係を注入するには、クラスのコンストラクターでその依存関係を宣言するだけです。先ほどのコード例では、次の行がその方法を示しています。
NestJS
NestJSは、高効率で信頼性が高く、拡張性に優れたバックエンドアプリケーションの構築に適したアーキテクチャを持つJavaScriptフレームワークです。設計においてAngularから多くの影響を受けているため、NestJSの依存性注入はAngularとよく似た仕組みで動作します。
NestJSとAngularでは、通常、依存関係はシングルトンとして扱われます。つまり、依存関係が見つかると、その値はキャッシュされ、アプリケーションのライフサイクルを通じて再利用されます。NestJSでこの動作を変更するには、@Injectableデコレーターのオプションで`scope`プロパティを設定します。Angularでは、@Injectableデコレーターのオプションで`providedIn`プロパティを設定します。
Vue.js
Vue.jsは、ユーザーインターフェースを構築するための宣言的でコンポーネントベースのJavaScriptフレームワークです。Vue.jsの依存性注入は、コンポーネントの作成時に`provide`と`inject`オプションを使って設定できます。
コンポーネントの子孫で利用できるデータを指定するには、provideオプションを使います。
必要なコンポーネントでは、inject オプションを使ってこれらの依存関係を注入できます。
AngularのprovidedIn: 'root'と同様に、アプリレベルの依存関係を提供するには、Vue.jsのアプリインスタンスからprovideメソッドを使います。
JavaScriptの依存性注入フレームワーク
ここまでは、JavaScriptフレームワークにおける依存性注入を見てきました。では、先ほどのフレームワークを使わずにプロジェクトを始めたい場合や、依存性注入コンテナー/リゾルバーを一から実装したくない場合はどうすればよいでしょうか。
このようなケースで依存性注入機能を提供するライブラリがいくつかあります。たとえばinjection-js、InversifyJS、TSyringeです。次のセクションではInversifyJSに焦点を当てますが、他のパッケージも確認して、ニーズにより合うものがないか検討するとよいでしょう。
InversifyJS
InversifyJSはJavaScript向けの依存性注入コンテナーです。実行時のオーバーヘッドをできるだけ抑えながら、優れたオブジェクト指向プログラミング(OOP)とIoCの実践を促進できるように設計されています。プロジェクトのリポジトリはGitHubで確認できます。
プロジェクトでInversifyJSを使うには、依存関係に追加する必要があります。ここでは、これまで使ってきたゲームの例にInversifyJSを設定します。まずnpm initで新しいプロジェクトを作成します。ターミナルを開き、次のコマンドを順番に実行してください。
次に、npmまたはYarnを使ってinversifyをプロジェクトの依存関係に追加します。
InversifyJSはMetadata Reflection APIに依存しているため、reflect-metadataパッケージをポリフィルとしてインストールし、使用する必要があります。
次に、空のファイルを2つ作成して、NSSConsoleクラスとGameReaderクラスのコードを追加します。
続いて、それぞれのファイルに次のコードを追加します。
最後に、InversifyJSを使うようプロジェクトを設定します。
プロジェクトのルートからnode index.mjsを実行すれば、新しい設定をテストできます。
シンプルな例ではありますが、InversifyJSを使う多くのプロジェクトにとって、よい出発点となるでしょう。
まとめ
この記事では、JavaScriptにおける依存性注入とそのメリット・デメリット、人気のJavaScriptフレームワークでの実装例、そしてバニラJavaScriptのプロジェクトでの使い方を紹介しました。



