generated at
TypeScriptを使ってDIについて説明する
DIとは?
Dependency Injectionの略です。
依存性の注入などとも呼びます。

コード例
言葉で説明するだけだとわかりにくいと思うので、Node.jsTypeScriptを例に実際にコードを載せてみます。
まず、記事( Article )の永続化に関して責任を持つ ArticleRepository という interface を定義します。
typescript
interface ArticleRepository { add(article: Article): Promise<void>; get(id: ArticleID): Promise<Article>; }
次に、SQLiteをベースに永続化機能を実装した SQLiteArticleRepository を用意します。この SQLiteArticleRepository は上記の ArticleRepository interface を実装します。
typescript
export class SQLiteArticleRepository implements ArticleRepository { readonly #database: Database; constructor(database: Database) { this.#database = database; } add(article: Article): Promise<void> { ... } get(id: ArticleID): Promise<Article> { ... } }
これらの interface class をベースに説明していきます
DIを使わない例
先程登場した ArticleRepository に依存する ArticleService というクラスがあったとします。このクラスの constructor 内では ArticleRepository の実装である SQLiteArticleRepository 直接生成されています。
typescript
class ArticleService { readonly #articleRepository: ArticleRepository; constructor() { // ArticleServiceの内部でSQLiteArticleRepositoryが生成されている this.#articleRepository = new SQLiteArticleRepository(new Database("/path/to/db")); } async addArticle(article: Article) { await this.#articleRepository.add(article); // ... } }
このようにすると、 ArticleService SQLiteArticleRepository の生成方法などの実装の詳細に関する知識を持つことになり、結合度も高くなってしまいます。また、カプセル化ポリモーフィズムといったOOPにおける大きなメリットも損なわれてしまっている状態です。

上記の例では constructor 内で SQLiteArticleRepository のインスタンスを直接生成していましたが、以下のように他のファイルから SQLiteArticleRepository のインスタンスを import して利用しているようなケースでも同様の問題が発生します。
typescript
import { sqliteArticleRepository } from "@/infra"; class ArticleService { async addArticle(article: Article) { // 別のファイルでexportされているSQLiteArticleRepositoryを直接使用している await sqliteArticleRepository.add(article); // ... } }
DIを利用した例
先程のDIが利用されていない例で登場したコードを以下のように変更してみましょう。
typescript
class ArticleService { readonly #articleRepository: ArticleRepository; constructor(articleRepository: ArticleRepository) { this.#articleRepository = articleRepository; } }
DIを利用しないコードにおいては ArticleRepository を実装した SQLiteArticleRepository ArticleService で直接生成または依存する形になっていました。これは ArticleService SQLiteArticleRepisoty 実装の詳細に強く依存した状態です。上記のコードでは constructor を介して ArticleRepository interface を受け取る形へ代わっています。こうすることで、 ArticleService SQLiteArticleRepisoty という詳細ではなく、 ArticleRepository という抽象に依存した形になります。

あるオブジェクトの生成方法というのは実装の詳細に当たるものです。 ArticleService ArticleRepository がどのように実装されているかを知る必要はなく、どのように生成すべきかについても知っておく必要はありません。このように分離することで、実装の詳細を切り離すことができ、結合度を低下させられます

また、こうすることで、 ArticleRepository を実装さえしていればどのようなオブジェクトでも渡せるため、用途に応じて実装を柔軟に差し替えることもできるようになります
例えば、テストコードを記述する際はフェイク実装やスタブなどに差し替えることもできます
typescript
// フェイク実装を利用する const articleRepository = new InMemoryArticleRepository(); const articleService = new ArticleService(articleRepository); // testdouble.jsを使用したスタブまたはモックへの差し替え import td from "testdouble"; const articleRepository = td.object<ArticleRepository>(); const articleService = new ArticleService(articleRepository);

基本
上記のように、DIそのものは複雑なテクニックではなくとても単純な仕組みで、特にライブラリやフレームワークなどを利用せずとも導入できます
あるオブジェクトが依存するオブジェクトは、自分自身で生成したりグローバル参照を介して依存をさせず、代わりにコンストラクタ引数を介して受け渡すようにします
コンストラクタ引数を介して受け取るオブジェクトの型を interface で宣言しておくことで、結合度を低下させることができ、再利用性やテストの容易性などの改善につながります。

DIのメリット/デメリット
メリット
結合度が低下し、モジュールのテスタビリティや再利用性の向上、変更による影響範囲の局所化などに繋がります
これは大規模なアプリケーションにおいては恩恵を受けやすいかと思います
例えば、上記の例で出てきた SQLiteArticleRepository にキャッシュの仕組みを導入したいとします
まず、キャッシュに関する interface を定義しておきます
typescript
export interface Cache { get(key: string): Promise<CacheEntry | null>; set(key: string, value: unknown): Promise<void>; delete(key: string): Promise<void>; } export interface CacheEntry { key: string; value: unknown; }
次に上記の Cache SQLiteArticleRepository に導入します。この際も、 constructor 経由で Cache interface を受け取らせます。
typescript
export class SQLiteArticleRepository implements ArticleRepository { readonly #database: Database; readonly #cache: Cache constructor(database: Database, cache: Cache) { this.#database = database; this.#cache = cache; } async get(id: ArticleID): Promise<Article> { const maybeCacheEntry = await this.#cache.get(id.toString()); if (maybeCacheEntry != null) { return this.#parseCacheEntry(cacheEntry); } const article = await this.#getArticleFromDB(id); await this.#cache.set(id.toString(), this.#serializeArticle(article)); return article; } // 省略... }
この変更は、キャッシュに依存した SQLiteArticleRepository の内部のみで完結しており、 SQLiteArticleRepository に依存した ArticleService などにはまったくコードの変更が行われていません
同様に、キャッシュを削除する場合でも透過的に変更を行うことができます
また、 SQLiteArticleRepository Cache interface (抽象)に依存しており、具体的な実装には依存していません。そのため、状況などに応じて柔軟に実装を差し替えることもできます。
typescript
// 本番用コードではRedisベースのCache実装を使う const redisCache = new RedisCache(redisClient); const articleRepository = new SQLiteArticleRepository( database, redisCache, ); const articleService = new ArticleService(articleRepository); // テストなどではインメモリ実装を使う const inMemoryCache = new InMemoryCache(); const articleRepository = new SQLiteArticleRepository( database, inMemoryCache, );
DIによって、このように変更の範囲を局所化したり、また実装を柔軟に変更したりすることが可能です
※(補足) 実際のアプリケーションを開発していく上では、上記の例で紹介したようなキャッシュの仕組みは SQLiteArticleRepository ではなく ArticleService の方に導入した方が適切なケースもあるかと思います。その場合も、 ArticleService には RedisCache InMemoryCache などの具体的な実装ではなく、 Cache interfaceに依存させるとよいでしょう。
デメリット
小規模なアプリなどの場合、やや面倒に感じるかもしれません
DIを採用すると、そうでない場合と比較して記述が面倒になってしまうのは事実だと思います
ただし、メンテナンス性を向上させるための様々な手法やライブラリというのは、ここで紹介したDIも含めて、大抵の場合、面倒になりがちなものだと思います
例:
レイヤードアーキテクチャーやその派生アーキテクチャー
ただし、こういったメンテナンス性を改善するための手法やライブラリなどは大抵、きちんとした背景があってそのようになっている場合がほとんどだと思います
そのため、アプリケーションや組織の規模などに応じて、採用する/しないを判断すると良いと思います
そのため、アプリケーションの規模などに応じて採用する/しないは判断するのがよいと思います
例えば、使い捨てのスクリプトなどを書く際はDIを使わなくてもまったく問題はありません
特定のフレームワークや言語においては、DIは一般的ではないかと思います
例えばRailsを使う場合は、素直にRails wayに従う方がよいかと思います
Railsだと、Rubyという言語が非常に柔軟かつ強力なのとインテグレーションテストを記述するための十分な基盤やエコシステム、ノウハウなどがRailsには存在することもあって、DIの使用は逆にあまり一般的ではないと思います
逆に、フレームワークの機能としてDIが提供されているケースもあります。そのような場合は、積極的に活用していくと良いのではないかと思います
有名なところで言うと、以下のようなフレームワークなどでDIが提供されています

DIの手法
手動DI
このページで示したコード例における手法です
主にアプリケーションのエントリポイントなどで、依存関係をあらかじめ手動でまとめて注入します
typescript:src/main.ts
async function bootstrap() { const config = await loadApplicationConfig(); const database = new Database(config.database.path); const articleRepository = new SQLiteArticleRepository(database); const articleService = new ArticleService(articleRepository); const articleController = new ArticleController(articleService); const apiServer = new APIServer(config, articleController); await apiServer.listen(); } if (import.meta.main) { await bootstrap(); }
依存関係を要求せずシンプルに実現ができ、柔軟なことなどがメリットだと思います

DIコンテナ
DIコンテナは依存関係の注入やオブジェクトのライフサイクル管理などを自動化する機能などを提供してくれます
Nest.jsAngularなどのフレームワークは組み込みでDIコンテナを提供しているため、それを活用すると良いと思います
それ以外のフレームワークなどでDIコンテナを使いたいときは、下記パッケージなどの使用を検討してみるとよいでしょう
InversifyJS - おそらく最もスター数が多くメジャーではないかと思います
TSyringe - Microsoft製のDIコンテナ
typedi - TypeORMの作者の方が開発しているDIコンテナ
@owja/ioc - 上記3つと異なり、reflect-metadataに依存せずに実装されているのが特徴です
手動でDIをするかDIコンテナを採用するかは好みの部分なども大きいと思うので、必要に応じて採用は判断するとよいと思います

コンストラクタインジェクション
このページで紹介したように、コンストラクタを介して依存オブジェクトを注入する方法です
手動DI/DIコンテナどちらの方法においてもコンストラクタインジェクションは利用可能です

setterインジェクション
以下のようにsetterメソッドを介して依存オブジェクトを注入する手法です
typescript
const parser = new Parser(); const lexer = new Lexer(); parser.setLexer(lexer); // setterを介して注入する
基本的にこの方法は推奨されません
setterを公開することにより注入を許可すると、そのオブジェクトの実装の詳細を外部に晒すことになってしまうためです (カプセル化によるメリットが損なわれてしまう)
特別な理由が無い限りはコンストラクタインジェクションを利用するとよいでしょう
typescript
const parser = new Parser(new Lexer()); // コンストラクターを介して注入する

プロパティインジェクション
特定のプロパティに対して依存関係を注入する方法です
InversifyJSなどでサポートされています
typescript
@injectable() class SQLiteUserRepository implements UserRepository { @inject(TYPES.DB_CONNECTION) private connection: DBConnection; }
これについてもsetterインジェクションと同様の利用で推奨はされず、コンストラクタインジェクションを利用するのが望ましいでしょう
個人的にはsetterインジェクションやプロパティインジェクションというのは、既存のDIが導入されていないアプリケーションにおいて、段階的にDIを導入していきたいようなケースで活用すると良いのではないかと思っています

その他のアプローチ・類似テクニック
あるオブジェクトや関数が依存するものは基本的に引数を介してやり取りさせるというのは、DIに限らず、結合度を低下させるための基本的な手法だと思います
そのため、JavaScriptにおいてはDI以外にも似たような手法などがいくつか見られます
ファクトリ関数を使ったテクニック
Koaのミドルウェアを使った例を紹介します。
javascript
import { redisCache } from '@/utils/redis-cache'; const cacheMiddleware = async (ctx, next) => { const key = generateCacheKey(ctx); const cachedItem = await redisCache.get(key); ... }; app.use(cacheMiddleware);
上記コードは cacheMiddleware がグローバルの redisCache インスタンスに直接依存しています
これを次のように変更してみます (JavaScriptにおいては、こういった特定のオブジェクトなどを生成する役割を持つ関数のことをファクトリ関数などと呼ぶケースがよく見かけられます)
typescript
import type { Cache } from "@/types/cache"; // middlewareを作成するファクトリ関数(createCacheMiddleware)を定義しています // この関数にはCacheインターフェースを実装したオブジェクトを引数として受け取らせます const createCacheMiddleware = (cache: Cache) => async (ctx, next) => { const key = generateCacheKey(ctx); const cachedItem = await cache.get(key); ... }; const redisCache = new RedisCache(redisClient); const cacheMiddleware = createCacheMiddleware(redisCache); app.use(cacheMiddleware);
こうすることで、用途に応じて cache をRedisベース以外の実装に差し替えたり、複数のミドルウェアを作成したいときなどにも柔軟に対応できます。
この例ではミドルウェアを例に説明しましたが、それ以外でも流用できます。

withExtraArgument を使うと、 thunk アクションに対して依存オブジェクトなどを注入できます
javascript
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; const deps = { api }; const store = createStore( reducer, applyMiddleware(thunk.withExtraArgument(deps)), ); function getArticle(id) { return (dispatch, getState, { api }) => { api.getArticle(id) .then(...) .catch(...) }; }

関連ページ

以下のあたり本にもっと詳しく書かれています