DI(Dependency Injection): 依存性の注入
DIコンテナーはアプリケーションが起動する前にすべての依存関係を解決し、必要に応じて必要なクラスのインスタンスを生成して注入する。
このため、ランタイム時に依存関係が解決され、必要なインスタンスが自動的に生成されます。これにより、アプリケーションのモジュールを単一の場所で管理でき、より柔軟なアプリケーション構造を実現できる。
DIとは
Dependency Injection (DI)は、オブジェクトの作成と依存関係の解決を分離することで、コードのテストや保守性を向上させるための設計パターンのひとつ。
このパターンを実現するためには、クラス間の依存関係を抽象化し、それをDIコンテナーに登録し、必要に応じて依存関係を注入する必要がある。
しかし、注入された依存関係が正しく解決されない場合、DIのメリットを十分に享受できなくなる。
つまり、DIコンテナーを使用しながらも、依然として依存関係がハードコーディングされたり、誤った型が注入されたりする可能性があるため、DIのメリットが十分に発揮されなくなるということです。
手動での依存性注入
このコードは、依存性を手動で解決し、インスタンス化して注入するアプローチを取っている。
これは、依存性の注入(DI: Dependency Injection)の原理にしたがっていますが、具体的には「手動での依存性注入」の例。
export const commandContainer = (): CommandRouter['controller'] => ({
...new CommandController(
controllerLogger,
new CommandResourceInteracts(usecaseLogger, new JwtAdapter(usecaseLogger), ClientAdapter),
),
});
一方、tsyringe
はDIを容易にするためのライブラリであり、「自動での依存性注入」を提供 する。
tsyringe
を使用した自動での依存性注入
tsyringe
はTypeScriptのための軽量な依存性注入コンテナーで、クラスのコンストラクターを通じて自動的に依存関係を解決します。tsyringe
を使用すると、依存関係を「登録」し、アプリケーションのどこからでもそれらを「解決」できるようになります。
import { container, injectable } from 'tsyringe';
@injectable()
class CommandController {
constructor(private logger: Logger, private commandResourceInteracts: CommandResourceInteracts) {}
}
container.register('Logger', { useClass: Logger });
container.register('CommandResourceInteracts', { useClass: CommandResourceInteracts });
const commandController = container.resolve(CommandController);
違い
- 明示性 vs. 暗黙性: 手動で の依存性注入は明示的であり、依存関係の構築を完全に制御できますが、
tsyringe
のようなDIコンテナを使用すると、依存関係の解決が暗黙的になり、コードがより宣言的になります。 - 管理の簡便性: 複雑なアプリケーションでは、
tsyringe
のようなDIコンテナを使用することで、依存関係の管理が簡単になります。DIコンテナは、依存関係のライフサイクル管理やスコープ管理を自動化し、コードの再利用性とテストのしやすさを向上させます。 - 設定の集中化:
tsyringe
を使用すると、依存関係の設定をアプリケーションの一箇所に集中化できるため、依存関係の構成が一目でわかりやすくなります。
結論として、tsyringe
のようなDIライブラリを使用する主な利点は、依存関係の解決を自動化し、管理を簡素化することにあります。これにより、アプリケーションの拡張性と保守性が向上しますが、DIライブラリの学習曲線と、その使用に伴う追加の抽象化レイヤーを受け入れる必要があります。
DIが動作する前提
DIコンテナー自体がそもそもオブジェクト指向プログラミング言語を前提としている。
DIメリット
DIはクラス間の依存性を下げるために使用される
通常、オブジェクトを生成するクラスは、そのオブジェクトが使用する他のオブジェクトのインスタンスを生成する必要があります。
これにより、各クラスが複雑に依存し合うことになり、修正や変更が困難になる可能性がある。
DIコンテナーを使用すると、各クラスが必要なオブジェクトを生成するのではなく、必要なオブジェクトをコンテナーに要求して、コンテナーからインスタンスを受け取ることができる。
このようにすることで各クラスが直接依存するオブジェクトを知る必要がなくなり、依存性が低下することになる。
たとえば、クラスAがクラスBとクラスCのインスタンスを生成して利用する場合、クラスAはクラスBとクラスCの詳細を知っている必要があります。
しかし、DIコンテナーを使用すると、クラスAは単にクラスBとクラスCが必要であることをコンテナーに通知し、コンテナーから受け取ることができます。
クラスAが直接クラスBとクラスCに依存しなくなるため、クラスAが変更されても、影響を受けるクラスが少なくなり、修正や変更が容易になります。
注入するタイミング
DIコンテナーファイルとしてクラスの対 応リストを保持しておく
事前に依存するオブジェクトを登録しておいて使う時になったらDIコンテナー経由でオブジェクトを取得する。
※この設定はスタートアップスクリプトなどに登録しておくようにする。
注入方法
何種類かある。
keyで渡す方法
@inject(key)
での注入は、DIコンテナーからキーにマッピングされた値を取得して、クラスのコンストラクターに注入する方法。
キーは、一般的に Symbol
型や文字列型などで定義され、クラスに紐づけられている。
キーでの注入は、DIコンテナーを使用して依存関係を解決するための標準的な方法であり、より柔軟で拡張性が高い。
クラスで渡す方法
クラスそのものを注入する方法は、コンストラクターに直接クラスのインスタンスを渡す方法。
この場合、クラスを手動でインスタンス化して、そのインスタンスをコンストラクターに渡す必要がある。
クラスそのものを注入する方法は、手動で依存関係を解決するための方法であり、一般的に柔軟性が低く、コードの保守性が低下する可能性がある。
依存関係を解決する方法は複数ありますが、クラスそのものを @inject() で渡すこともできます。ただし、この方法はクラスに変更があった場合、他のクラスに影響がある可能性があるため、クラスを変更する場合は注意が必要です。通常、DIコンテナはキーを使用して依存関係を解決するため、この方法はキーを省略することができるという利点がありますが、依然としてクラスが注入されるという点は変わりありません。