TypeScriptにおけるInversifyJSまたはTSyringeを用いたIoCのマスター
James Reed
Infrastructure Engineer · Leapcell

はじめに
現代のソフトウェア開発の複雑な世界では、堅牢で保守性の高いアプリケーションを構築することは、コードベースのさまざまな部分間の依存関係をいかに効果的に管理するかにかかっています。TypeScriptはその強力な型付けとスケーラブルな性質により、ますます普及しており、アプリケーションを疎結合でテストが容易な状態に保つことが不可欠になっています。ここで、Inversion of Control(IoC)の概念が真価を発揮します。IoC、特にDependency Injection(DI)フレームワークを通じて実装されるものは、コンポーネントが自身の依存関係を作成するのではなく、外部エンティティに依存関係を管理させることで、コンポーネントを疎結合にします。このパラダイムシフトは、モジュール性を大幅に向上させ、単体テストを容易にし、最終的にはより回復力があり適応性の高いソフトウェアにつながります。TypeScriptエコシステムでは、InversifyJSとTSyringeがこれを実現するための強力なツールとして際立っており、IoCを実装するためのエレガントなソリューションを提供します。この記事では、IoCのコア原則を掘り下げ、これらの2つのフレームワークが開発者をより良いTypeScriptアプリケーションの構築にどのように力を与えるかを探ります。
Inversion of ControlとDependency Injectionの理解
InversifyJSとTSyringeの具体例に入る前に、それらが活用する基本的な概念を明確に理解しましょう。
Inversion of Control (IoC) は、プログラムの制御フローが反転される設計原則です。アプリケーションが再利用可能なライブラリを呼び出すのではなく、フレームワークがアプリケーションのコンポーネントを呼び出します。「私たちを呼ぶな、私たちがあなたを呼びます」というハリウッドの原則と考えてください。コンポーネント間インタラクションの文脈では、コンポーネントが依存関係を作成または管理するのではなく、外部メカニズムがそれらを提供するということです。
Dependency Injection (DI) はIoCの具体的な実装です。これは、オブジェクトが依存する他のオブジェクトを受け取るテクニックです。これらの「依存関係」は、オブジェクトがそれ自体で作成したり、サービスロケーターから取得したりするのではなく、実行時に依存オブジェクトに注入されます。依存関係はいくつかの方法で注入できます。
- コンストラクタインジェクション: 依存関係はクラスコンストラクタを通じて提供されます。これは、オブジェクトがすべての必要な依存関係とともに有効な状態で作成されることを保証するため、しばしば好まれます。
- セッターインジェクション: 依存関係はパブリックセッターメソッドを通じて提供されます。これにより、オプションの依存関係や、オブジェクト作成後に依存関係を変更することが可能になります。
- インターフェースインジェクション: 依存関係は、クライアントクラスが実装するインターフェースを通じて提供されます。(他の言語と比較してTypeScriptではあまり一般的ではありません)。
DIの利点は多岐にわたります。
- 疎結合: コンポーネントは実装に緊密に結合されていないため、変更または置換が容易になります。
- テスト容易性: 実際のインプリメンテーションの代わりにテストダブルを注入できるため、単体テストのための依存関係のモック化が簡単になります。
- 保守性: 明確な依存関係により、コードの理解と保守が容易になります。
- 再利用性: コンポーネントは依存関係をハードコーディングしないため、さまざまなコンテキストでより再利用可能になります。
これらの概念を踏まえ、InversifyJSとTSyringeがTypeScriptでこれらの原則を実装するのをどのように支援するかを見てみましょう。
InversifyJSの詳細
InversifyJSは、TypeScriptおよびJavaScriptアプリケーション向けの強力で非常に人気のあるIoCコンテナです。デコレータと型メタデータを利用して依存関係を定義および注入します。
コア原則とセットアップ
InversifyJSは3つの主要な概念で動作します。
- インターフェース/型: サービス契約を定義します。これにより、強力な型付けと疎結合が促進されます。
- クラス(実装): これらのインターフェースの具体的な実装を提供します。
- コンテナ: インターフェースとそれらの具体的な実装をバインドし、インスタンスを解決する場所です。
インストール:
npm install inversify reflect-metadata npm install @types/reflect-metadata --save-dev
また、tsconfig.json
でemitDecoratorMetadata
とexperimentalDecorators
を有効にする必要があります。
{ "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["es6"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, "outDir": "./dist" } }
そして、アプリケーションのエントリポイントでreflect-metadata
を一度インポートします。
import "reflect-metadata";
例の実装
ロギングサービスを必要とするシンプルなアプリケーションを構築していると想像してみましょう。
1. インターフェース/型の定義:
InversifyJSでは、インターフェース自体に加えて、インターフェースの識別子としてSymbol
またはstring
定数を定義することが一般的です。
// interfaces.ts export interface ILogger { log(message: string): void; } export const TYPES = { Logger: Symbol.for("Logger"), };
2. サービスのインプリメンテーション:
// services.ts import { injectable } from "inversify"; import { ILogger } from "./interfaces"; @injectable() export class ConsoleLogger implements ILogger { public log(message: string): void { console.log(`[ConsoleLogger] ${message}`); } } @injectable() export class FileLogger implements ILogger { public log(message: string): void { console.log(`[FileLogger] Saving to file: ${message}`); // In a real app, this would write to a file } }
@injectable()
デコレータに注意してください。これは、InversifyJSによって注入の候補としてクラスをマークします。
3. ロガーを使用するサービスの定義:
// app.ts import { injectable, inject } from "inversify"; import { ILogger, TYPES } from "./interfaces"; @injectable() export class Application { private _logger: ILogger; constructor(@inject(TYPES.Logger) logger: ILogger) { this._logger = logger; } public run(): void { this._logger.log("Application started!"); } }
ここで、@inject(TYPES.Logger)
は、InversifyJSにILogger
のインスタンスをコンストラクタに注入するように指示します。
4. IoCコンテナの設定:
// container.ts import { Container } from "inversify"; import { TYPES, ILogger } from "./interfaces"; import { ConsoleLogger, FileLogger } from "./services"; import { Application } from "./app"; const container = new Container(); // ロガーインターフェースをその実装にバインド container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger); // または .to(FileLogger) // Applicationクラスをバインド container.bind<Application>(Application).toSelf(); // クラス自体にクラスをバインド export default container;
ここで、container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger)
が魔法なのです。コンテナに「TYPES.Logger
を要求するたびに、ConsoleLogger
のインスタンスを提供する」と指示します。
5. 解決と実行:
// main.ts import container from "./container"; import { Application } from "./app"; // コンテナからApplicationのインスタンスを取得 const app = container.get<Application>(Application); app.run(); // 出力: [ConsoleLogger] Application started!
スコープ
InversifyJSはさまざまなバインディングスコープをサポートしています。
.toConstantValue(value)
: 常に同じ既存の値が返されます。.toDynamicValue(factory)
: ファクトリ関数によって生成された値が返されます。.toSelf()
: クラス自体にクラスをバインドします(サービスと実装の両方であるクラスに便利です)。.inSingletonScope()
: 要求されるたびに同じインスタンスが返されます。.inTransientScope()
(デフォルト): 要求されるたびに新しいインスタンスが返されます。
シングルトンの例:
container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger).inSingletonScope();
container.ts
のConsoleLogger
をFileLogger
に切り替えると、Application
やmain.ts
を変更せずにロギングの動作が変更され、IoCの威力が示されます。
TSyringeの詳細
TSyringeは、TypeScriptおよびJavaScriptの軽量な依存性注入コンテナで、依存関係を管理および注入するための簡単な方法を提供します。Microsoftによって開発されており、TypeScriptの実験的なデコレータとReflectメタデータを利用した、InversifyJSよりも軽量な代替手段と見なされることがよくあります。
コア原則とセットアップ
TSyringeはInversifyJSと非常によく似たパターンに従っており、クラスのデコレーションとグローバルコンテナ(または好みに応じて特定のコンテナ)の使用に焦点を当てています。
インストール:
npm install tsyringe reflect-metadata npm install @types/reflect-metadata --save-dev
InversifyJSと同様に、tsconfig.json
でemitDecoratorMetadata
とexperimentalDecorators
を有効にし、アプリケーションのエントリポイントでreflect-metadata
を一度インポートする必要があります。
{ "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["es6"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, // デバッグを推奨 "outDir": "./dist" } }
そしてreflect-metadata
をインポートします。
import "reflect-metadata";
例の実装
TSyringeを使用してロギング例を再実装してみましょう。
1. インターフェース/トークンの定義:
InversifyJSとは異なり、Symbol
がトークンによく使用されますが、TSyringeは通常、登録のために文字列またはクラス(インターフェースを表す「トークン」としてのクラス)を使用します。インターフェースの場合、InjectionToken
が推奨される方法です。
// interfaces.ts import { InjectionToken } from "tsyringe"; export interface ILogger { log(message: string): void; } export const ILoggerToken: InjectionToken<ILogger> = "ILogger";
2. サービスのインプリメンテーション:
TSyringeはInversifyJSと同様に@injectable()
デコレータを使用します。
// services.ts import { injectable, registry } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; @injectable() // ILoggerTokenの実装としてこのクラスを自動的に登録します。 // これは、単純なケースでは中央コンテナでの明示的なバインド呼び出しを置き換えます。 @registry([{ token: ILoggerToken, useClass: ConsoleLogger }]) export class ConsoleLogger implements ILogger { public log(message: string): void { console.log(`[ConsoleLogger (TSyringe)] ${message}`); } } @injectable() export class FileLogger implements ILogger { public log(message: string): void { console.log(`[FileLogger (TSyringe)] Saving to file: ${message}`); // In a real app, this would write to a file } }
@registry
デコレータに注意してください。これは、クラス定義レベルで具体的なクラスをインターフェイストークンに直接関連付ける便利な方法です。これにより、単純なケースのセットアップが簡素化されます。または、後述するcontainer.register
を手動で使用することもできます。
3. ロガーを使用するサービスの定義:
TSyringeは、InversifyJSと同様に、メリットインジェクションのために@inject()
を使用します。
// app.ts import { injectable, inject } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; @injectable() export class Application { private _logger: ILogger; constructor(@inject(ILoggerToken) logger: ILogger) { this._logger = logger; } public run(): void { this._logger.log("Application started!"); } }
4. IoCコンテナの設定(@registry
を広範に使用しない場合):
TSyringeは、インポートして使用できるグローバルコンテナcontainer
を提供します。
// main.ts (または専用のDIセットアップファイル) import { container } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; import { ConsoleLogger, FileLogger } from "./services"; import { Application } from "./app"; // 手動登録(ConsoleLoggerの@registryの代替) // container.register<ILogger>(ILoggerToken, { useClass: ConsoleLogger }); // FileLoggerに切り替えるには: container.register<ILogger>(ILoggerToken, { useClass: FileLogger }); // Applicationの場合、直接注入可能で依存関係を取るため、依存関係が解決されれば登録する必要はありません。 // ただし、後で明示的に解決したい場合は、登録する場合があります。 // container.register(Application, { useClass: Application }); // または container.resolve(Application) は、依存関係が満たされれば直接機能します。 // コンテナからApplicationのインスタンスを取得 const app = container.resolve(Application); // TSyringeは、すべてのコンストラクタの依存関係が解決されていれば、クラスを直接解決できます。 app.run(); // 出力: [FileLogger (TSyringe)] Saving to file: Application started!
スコープ
TSyringeは、さまざまなライフタイムスコープもサポートしています。
- トランジェント(デフォルト): 要求されるたびに新しいインスタンスが作成されます。
container.register(ILoggerToken, { useClass: ConsoleLogger });
- シングルトン: 要求されるたびに同じインスタンスが返されます。
container.registerSingleton(ILoggerToken, ConsoleLogger); // または、シングルトンとしてクラスを直接登録する場合 container.registerSingleton(Application);
- スコープ付き: インスタンスは特定のスコープ(例:Webリクエスト)内で共有されます。TSyringeでは、この目的のために子コンテナを作成できます。
シングルトンの例:
import { container } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; import { ConsoleLogger } from "./services"; import { Application } from "./app"; container.registerSingleton<ILogger>(ILoggerToken, ConsoleLogger); const app = container.resolve(Application); app.run(); // 出力: [ConsoleLogger (TSyringe)] Application started!
InversifyJSとTSyringeの選択
InversifyJSとTSyringeはどちらもTypeScriptアプリケーションでIoCを実装するための優れた選択肢ですが、微妙な違いがあり、決定に影響を与える可能性があります。
- 成熟度とエコシステム: InversifyJSはより長く存在しており、より大きなコミュニティ、より多くの例、そしておそらく他のフレームワークとのより多くの統合があります。TSyringeは、Microsoftによってバックアップされていますが、広範な普及についてはやや新しいものです。
- 軽量 対 高機能: TSyringeは一般的に軽量で、特に基本的なDIでは、よりシンプルなAPIと見なされています。InversifyJSは、拡張可能なバインディング構文、コンテナ解決のためのミドルウェア、およびバインディングプロセスに対するより詳細な制御を含む、より豊富な機能セットを提供します。
Symbol
対string
/class
トークン: InversifyJSは、文字列リテラルの衝突を回避するためにトークンとしてSymbol
を推奨することがよくありますが、文字列もサポートされています。TSyringeは、単純なケースではより読みやすい場合があるトークンとして文字列またはクラス参照を好みます。- 構成: TSyringeの
@registry
デコレータは、単純なケースの構成ファイルを大量に用意する必要性を減らす、非常に簡潔なインラインサービス登録を可能にします。InversifyJSは通常、Container
インスタンス内にバインディングを集中させます。大規模なアプリケーションでは、集中化された構成が監視に役立つことがよくあります。 - エラー処理: どちらのフレームワークも解決不可能な依存関係に対して適切なエラーメッセージを提供しますが、診断機能は若干異なる場合があります。
InversifyJSを選択する場合:
- 高度に構成可能で拡張可能なIoCコンテナが必要な場合。
- カスタム解決フック、特定のバインディング構文、または詳細なデバッグ情報のような高度な機能が価値のある複雑なアプリケーションを構築している場合。
- 明示的で集中化されたバインディング構成を好む場合。
TSyringeを選択する場合:
- シンプルさと軽量なソリューションを優先する場合。
@registry
を介したインライン登録の利便性を高く評価する場合。- Microsoftによってバックアップされたライブラリが追加の信頼性を提供する環境で作業している場合。
- DIのニーズが主にコンストラクタインジェクションとシンプルなスコープである場合。
最終的には、どちらのフレームワークもIoCとDependency Injectionを効果的に実装するという同じ目標を達成します。選択はしばしば個人的な好み、プロジェクトの要件、およびそれらのそれぞれのAPIへの精通度にかかっています。
結論
Dependency InjectionによるInversion of Controlの実装は、堅牢で保守性が高く、テスト可能なTypeScriptアプリケーションを構築するための基盤となります。コンポーネントを疎結合にし、依存関係管理を外部化することで、アーキテクチャの柔軟性の新たなレベルを解き放ちます。InversifyJSとTSyringeは、それぞれ独自の強みとニュアンスを備えた、これを達成するための強力でデコレータ駆動のメカニズムを提供します。InversifyJSの豊富な機能拡張性またはTSyringeの合理化されたシンプルさを選択するかどうかにかかわらず、TypeScriptプロジェクトでIoCコンテナを採用することは、間違いなくよりクリーンなコードとより楽しい開発体験につながります。実装を簡単に切り替えたり、個々のコードユニットを厳密にテストしたりする機能は、これらのフレームワークが一貫して提供する貴重な利点です。