NestJS以外での依存性注入:tsyringeとInversifyJSの詳細解説
Wenhao Wang
Dev Intern · Leapcell

分離の力:TypeScriptプロジェクトにおける依存性注入
現代のソフトウェア開発の進化する状況において、スケーラブルで保守可能、かつテスト可能なアプリケーションを構築することは最重要です。これらの目標達成に大きく貢献するアーキテクチャパターンの一つが、依存性注入(DI)です。NestJSのようなフレームワークはDIをシームレスに統合していますが、このエコシステム外の多くのTypeScriptプロジェクトもその原則から大きな恩恵を受けることができます。この記事では、TypeScript向けの主要なDIコンテナであるtsyringe
とInversifyJS
が、独立したプロジェクトをどのように強化し、モジュール性を促進し、密結合を軽減できるかを探ります。それぞれのコアメカニズム、実装の詳細、および実際的なユースケースを検討し、ニーズに合った適切なツールを選択するのに役立ちます。
コアコンセプトの理解
tsyringe
とInversifyJS
の詳細に入る前に、いくつかの基本的なDIコンセプトについての理解をすぐに固めましょう。
- 依存性注入(DI): コンポーネントが依存関係を内部で作成するのではなく、外部ソースから受け取るデザインパターンです。この「制御の反転」は疎結合を促進し、コンポーネントのテスト、再利用、保守を容易にします。
- IoCコンテナ(制御の反転コンテナ): DIコンテナとしても知られ、オブジェクトとその依存関係のインスタンス化とライフサイクルを管理するフレームワークまたはライブラリです。設定やデコレータに基づいてコンポーネントの「配線」を処理します。
- バインディング/登録: DIコンテナに、要求されたときに特定の依存関係のインスタンスをどのように提供するかを通知するプロセスです。これには、インターフェースまたは抽象クラスを具体的な実装にマッピングすることがよく含まれます。
- 解決: DIコンテナから依存関係のインスタンスを要求する行為です。コンテナは、バインディングを検索し、必要なコンポーネントをインスタンス化し、それらを注入します。
- デコレータ: クラス、メソッド、プロパティ、またはパラメータにアタッチできる特別な種類の宣言です。DIの文脈では、デコレータはクラスを注入可能としてマークしたり、注入ポイントを定義したり、バインディングを設定したりするためによく使用されます。
- サービス識別子: IoCコンテナ内で特定の依存関係を一意に識別するために使用される一意のキー(多くの場合、文字列、シンボル、またはクラスコンストラクタ)です。
tsyringe:軽量でデコレータ駆動のアプローチ
Microsoftによって開発されたtsyringe
は、TypeScript向けの軽量でパフォーマンスの高い依存性注入コンテナです。TypeScriptの実験的なデコレータとリフレクション機能を利用して、依存関係管理を大幅に簡素化します。そのAPIは直感的で、「TypeScriptネイティブ」という感じがします。
実装と応用
実践的な例、つまりシンプルな通知サービスでtsyringe
を実証しましょう。
まず、tsyringe
をインストールします。
npm install tsyringe reflect-metadata
そして、tsconfig.json
でemitDecoratorMetadata
とexperimentalDecorators
が有効になっていることを確認します。
{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, "//": "...その他のオプション" } }
次に、サービスを定義しましょう。
// services/notifier.ts import { injectable } from "tsyringe"; interface INotifier { send(message: string): void; } @injectable() class EmailNotifier implements INotifier { send(message: string): void { console.log(`Sending email: ${message}`); } } @injectable() class SMSNotifier implements INotifier { send(message: string): void { console.log(`Sending SMS: ${message}`); } } export { INotifier, EmailNotifier, SMSNotifier };
次に、notifierを使用するサービスです。
// services/user-service.ts import { injectable, inject } from "tsyringe"; import { INotifier } from "./notifier"; @injectable() class UserService { constructor(@inject("INotifier") private notifier: INotifier) {} registerUser(username: string): void { console.log(`User ${username} registered.`); this.notifier.send(`Welcome, ${username}!`); } } export { UserService };
最後に、アプリケーションをブートストラップし、依存関係を解決します。
// app.ts import "reflect-metadata"; // エントリファイルの先頭で一度だけインポートする必要があります import { container } from "tsyringe"; import { INotifier, EmailNotifier, SMSNotifier } from "./services/notifier"; import { UserService } from "./services/user-service"; // 依存関係の登録ed // インターフェースを具体的な実装にバインドできます container.register<INotifier>("INotifier", { useClass: EmailNotifier }); // インターフェースが使用されていない場合や複数の実装がある場合は、クラスタイプで直接登録することもできます // container.register(EmailNotifier); // container.register(SMSNotifier); // この時点で、notifierを変更したい場合、上記の登録を変更するだけで済みます // 例えば、SMSNotifierを使用するには: // container.register<INotifier>("INotifier", { useClass: SMSNotifier }); // UserServiceの解決(これはINotifierを推移的に解決します) const userService = container.resolve(UserService); userService.registerUser("Alice"); // 一時的なサービス(毎回新しいインスタンス)とシングルトン(インスタンスは1つ)を持つこともできます // デフォルトでは、tsyringeクラスは指定されない限り一時的です。 // EmailNotifierをシングルトンにするには: // container.registerSingleton<INotifier>("INotifier", EmailNotifier);
この例では、@injectable()
はクラスを注入可能としてマークします。@inject("INotifier")
は、"INotifier"
という識別子で識別されたインスタンスを注入するようにtsyringe
に指示します。container.register()
メソッドは、INotifier
識別子をEmailNotifier
の具体的な実装にバインドする場所です。この明確な分離により、UserService
を変更せずに実装を簡単に切り替えることができます(例:EmailNotifier
からSMSNotifier
へ)。
tsyringe
は、そのシンプルさとTypeScriptの型システムとの密接な統合により、多くのプロジェクトにとって素晴らしい選択肢です。
InversifyJS:堅牢、柔軟、機能豊富
InversifyJS
は、TypeScript向けのもう一つの強力で広く採用されている依存性注入フレームワークです。tsyringe
と比較していくつかの高度なバインディングオプション、ライフサイクル管理、ミドルウェアサポートなどの、より広範な機能セットを提供します。拡張性とテスト容易性を念頭に置いて構築されています。
実装と応用
通知サービスの例をInversifyJS
に適応させましょう。
まず、InversifyJS
とreflect-metadata
をインストールします。
npm install inversify reflect-metadata
ここでも、tsconfig.json
でemitDecoratorMetadata
とexperimentalDecorators
が有効になっていることを確認します。
インターフェースを定義し、文字列ベースの魔法の文字列表現を避けるために、より良い型安全性と、InversifyJS
でよく使われるプラクティスであるシンボルをサービス識別子として使用します。
// services/notifier.ts import { injectable } from "inversify"; interface INotifier { send(message: string): void; } @injectable() class EmailNotifier implements INotifier { send(message: string): void { console.log(`[InversifyJS] Sending email: ${message}`); } } @injectable() class SMSNotifier implements INotifier { send(message: string): void { console.log(`[InversifyJS] Sending SMS: ${message}`); } } // サービス識別子のシンボルを定義 const TYPES = { INotifier: Symbol.for("INotifier"), }; export { INotifier, EmailNotifier, SMSNotifier, TYPES };
次に、UserService
はシンボルを使用して@inject
を使用します。
// services/user-service.ts import { injectable, inject } from "inversify"; import { INotifier, TYPES } from "./notifier"; @injectable() class UserService { constructor(@inject(TYPES.INotifier) private notifier: INotifier) {} registerUser(username: string): void { console.log(`[InversifyJS] User ${username} registered.`); this.notifier.send(`Welcome, ${username}!`); } } export { UserService };
最後に、InversifyJS
を使ったアプリケーションのブートストラップです。
// app.ts import "reflect-metadata"; // エントリファイルの先頭で一度だけインポートする必要があります import { Container } from "inversify"; import { INotifier, EmailNotifier, SMSNotifier, TYPES } from "./services/notifier"; import { UserService } from "./services/user-service"; // 新しいInversifyJSコンテナを作成 const inversifyContainer = new Container(); // バインディング: // シンボルを具体的な実装にバインドします inversifyContainer.bind<INotifier>(TYPES.INotifier).to(EmailNotifier); // シングルトンにすることもできます // inversifyContainer.bind<INotifier>(TYPES.INotifier).to(EmailNotifier).inSingletonScope(); // UserServiceを直接バインド inversifyContainer.bind<UserService>(UserService).toSelf(); // サービスを解決 const userService = inversifyContainer.get<UserService>(UserService); userService.registerUser("Bob"); // 実装の変更を実証するために console.log("\nSwitching to SMS Notifier:"); inversifyContainer.unbind(TYPES.INotifier); // 前のバインディングを解除 inversifyContainer.bind<INotifier>(TYPES.INotifier).to(SMSNotifier); // 新しいものをバインド const userServiceWithSMS = inversifyContainer.get<UserService>(UserService); userServiceWithSMS.registerUser("Charlie");
InversifyJS
は、@injectable
、@inject
といった同様のデコレータを使用しますが、通常はサービス識別子としてシンボルとペアで使われ、型安全性を提供し、文字列リテラルの衝突の可能性を回避します。Container.bind()
メソッドは、スコープ(.inSingletonScope()
、.inTransientScope()
など)を指定することを含む、バインディングを設定するための流暢なAPIを提供します。これにより、インスタンスのライフサイクルを細かく制御できます。その柔軟性は、DIプロセスに対するきめ細やかな制御が有益な、より大きく、より複雑なアプリケーションに適しています。
どちらを選ぶか?
tsyringe
とInversifyJS
は、どちらもTypeScriptプロジェクトにおける依存性注入のための優れた選択肢です。決定はしばしば、特定のプロジェクトのニーズと好みに左右されます。
-
tsyringe
を選ぶ場合:- より軽量なソリューションと最小限のAPIを好む場合。
- シンプルさとTypeScriptの型に密接に沿った直接的でデコレータ駆動のアプローチを重視する場合。
- プロジェクトが高度なバインディング構成や広範なライフサイクル管理機能を必要としない場合。
- 小規模から中規模のアプリケーションを構築している場合。
-
InversifyJS
を選ぶ場合:- より機能が豊富で堅牢なDIコンテナが必要な場合。
- プロジェクトで高度なバインディングオプション(例:条件付きバインディング、カスタムプロバイダー、マルチインジェクション)が必要な場合。
- インスタンスのライフサイクル(シングルトン、リクエストスコープなど)を細かく制御したい場合。
- 拡張性とテスト容易性が最重要視される、より大規模なエンタープライズグレードのアプリケーションに取り組んでいる、または堅牢なエラー処理とデバッグサポートが必要な場合。
- サービス識別子にシンボルを使用することを好み、型安全性を向上させる場合。
結論
NestJSの明示的な制約の外のtsyringe
やInversifyJS
のようなフレームワークで依存性注入を受け入れることは、TypeScriptアプリケーションの設計と構築方法を根本的に変革します。コンポーネントを分離し、それらの作成と配線を一元化することで、モジュール性、テスト容易性、保守容易性を向上させます。tsyringe
の洗練されたシンプルさを選択するにしても、InversifyJS
の包括的なパワーを選択するにしても、どちらもよりクリーンで回復力のあるコードを書くことを可能にし、最終的にはより堅牢でスケーラブルなソフトウェアシステムにつながります。両者の選択は、プロジェクトの特定の要求と複雑さに依存しますが、DIの根本的な利点は普遍的に価値があります。