Node.js で DI (Dependency Inversion) と DI (Dependency Injection) コンテナを試してみた

2023.08.10

こんにちは、CX事業本部 Delivery部の若槻です。

最近 DI コンテナ というものに初めて取り組む機会があったのですが、DI には以下のような似た用語や概念が多くあるようです。

  • DI (Dependency Inversion)
  • DI (Dependency Injection)
  • DI コンテナ

これらの理解に結構苦労したので実際に試しつつ整理してみました。

DI (Dependency Inversion)

Dependency Inversion (DI:依存関係逆転) とは、オブジェクト指向設計(およびレイヤーパターン)において、低レベルのモジュール(下位レイヤー)が実装する必要があるインターフェイスを高レベルのモジュール(上位レイヤー)が決定するようにする実装です。この方法論を依存関係逆転の原則 (DIP)と呼びます。

以下の図は Wikipediaからの引用ですが、従来や上位レイヤーが下位レイヤーに依存しているのに対して、依存関係逆転パターンでは下位レイヤーが上位レイヤー側にある interface(抽象化)に依存するようになります。

  • 従来のレイヤーパターン

  • 依存関係逆転のパターン

これにより上位レイヤーは下位レイヤーの仕様を意識する必要がなくなり、interface のみ意識すればよくなるので、実装を安定させることができます。

実例

Dependency Inversion の実例として次の記事の内容を参考に試してみます。

下記では、IAnimalRepository が 上位レイヤー側にある interface に依存しており、Animal は interface を通して下位レイヤーの AnimalRepository を使用しています。

src/Infrastructure/AnimalRepository.ts

import { IAnimalRepository } from '../Domain/Animal';

export class AnimalRepository implements IAnimalRepository {
  constructor(private id: number) {
    this.id = id;
  }

  findById() {
    if (this.id === 1) {
      console.log('dog');
    } else {
      console.log('cat');
    }
  }
}

src/Domain/Animal.ts

import { AnimalRepository } from '../Infrastructure/AnimalRepository';

export interface IAnimalRepository {
  findById(): void;
}

class Animal {
  constructor(private animalRepository: IAnimalRepository) {
    this.animalRepository = animalRepository;
  }

  get() {
    this.animalRepository.findById();
  }
}

const animalRepository = new AnimalRepository(1);
const animal = new Animal(animalRepository);
animal.get();

Animal class を使用する場合は次のようになります。

src/main.ts

import { Animal } from './Domain/Animal';
import { AnimalRepository } from './Infrastructure/AnimalRepository';

const animalRepository = new AnimalRepository(1);
const animal = new Animal(animalRepository);
animal.get(); // dog

ちなみに参考記事には、従来のパターンから依存性逆転パターンへの変更の推移も記載されており、両者の比較が非常に分かりやすく紹介されているので是非ごらんください。

DI (Dependency Injection) コンテナ

さて、前述の DI (Dependency Inversion) の実例では、上位レイヤーの Animal class の使用で上位および下位の2つの class を初期化する必要があるので、利用が増えるとコードの記述が冗長となっていきます。

そこで Dependency Injection(DI:依存性注入)という方法を用います。Dependency Injection は、依存関係の作成とバインドを依存クラスの外部で行うソフトウェア設計手法です。バインド後は前述の依存関係がすでにインスタンス化され、すぐに使用できるように提供されるため、「インジェクション」という用語が使用されます。

そして、Dependency Injection を簡単に実現するためのフレームワークは DI(Dependency Injection)コンテナと呼ばれます。

実例

Node.js では DI コンテナを実現するための代表的なライブラリとして、InversifyJS があります。

この InversifyJS を使って、前述の実例の実装を DI コンテナを使用したものに書き換えてみます。

必要なパッケージをインストールします。

npm i -D inversify reflect-metadata

コンテナ化する class には@injectable()、引数に対しては@inject()を使用して注入可能にします。

src/Infrastructure/AnimalRepository.ts

import { injectable, inject } from 'inversify';
import { IAnimalRepository } from '../Domain/Animal';

@injectable()
export class AnimalRepository implements IAnimalRepository {
  constructor(@inject('id') private id: number) {}

  findById() {
    if (this.id === 1) {
      console.log('dog');
    } else {
      console.log('cat');
    }
  }
}

src/Domain/Animal.ts

import { injectable, inject } from 'inversify';

export interface IAnimalRepository {
  findById(): void;
}

@injectable()
export class Animal {
  constructor(
    @inject('IAnimalRepository') private animalRepository: IAnimalRepository
  ) {}

  get() {
    this.animalRepository.findById();
  }
}

そして、DI コンテナを使用するための設定ファイルを作成し、ここで各 class をバインドしてコンテナを作成します

src/inversify.config.ts

import { Container } from 'inversify';
import { Animal, IAnimalRepository } from './Domain/Animal';
import { AnimalRepository } from './Infrastructure/AnimalRepository';

const myContainer = new Container();
myContainer.bind<number>('id').toConstantValue(1);
myContainer.bind<IAnimalRepository>('IAnimalRepository').to(AnimalRepository);
myContainer.bind<Animal>(Animal).toSelf();

export { myContainer };

最後に使用したい class のコンテナを取得して実行します。

import 'reflect-metadata';
import { Animal } from './Domain/Animal';
import { myContainer } from './inversify.config';

const animal = myContainer.get<Animal>(Animal);
animal.get(); // dog

DI コンテナを使用しない場合と比べて、class 使用側のコードの記述量を削減できていますね。

おわりに

Node.js で DI (Dependency Inversion) と DI (Dependency Injection) コンテナを試してみました。DI (Dependency Injection) コンテナを利用して DI (Dependency Inversion) より簡潔に実装することができました。両者は一緒に使われることが多いことが混同されやすい要因でしょうか。区別するように気をつけたいですね。

そして DI コンテナを使用することによるメリットは、コードの記述量の削減だけではなく、テストのしやすさや、依存関係の変更による影響範囲の狭さなどもあるので、今後使用していく中でより便利な活用方法を模索していきたいと思います。

合わせて読みたい

冒頭紹介の記事と同様に Dependency Inversion を図解付きで解説している。

詳細な解説が嬉しい。

弊社の吉川の記事。

今回紹介した概念の比較を詳しく紹介している。

以上