[JS] DIを理解する

フレームワークを用いた開発を経験したことがある人は、DI という言葉を耳にしたことがあるのではないでしょうか。

また、聞いたことがあるのに、実際それが何かと言われると説明が難しかったりもするのではないでしょうか。

この記事ではフレームワークの一部としての使い方ではなく、デザインパターンとしての DI を説明していこうと思います。

DI とは

DI (Dependency Injection) とはデザインパターンのひとつで、依存する値やオブジェクトを外部から注入できるようにクラス設計をすることを指します。

たとえば、以下の様な JavaScript のクラスがあるとします。

class SampleService {
  constructor() {
    this.repository = new SampleRepository()
  }

  async create(name) {
    const sample = new Sample(name)
    await this.repository.save(sample)
  }
}

const sampleService = new SampleService()

await sampleService.create("piyo")

(A)を動かすのに、(B)が必要である状態のことを、(A)が(B)に依存していると言います。

今回の場合は SampleService は SampleRepository に依存しています。

これを DI を用いて書き直すとこうなります。

class SampleService {
  constructor(repository) {
    this.repository = repository
  }

  async create(name) {
    const sample = new Sample(name)
    await this.repository.save(sample)
  }
}

const sampleRepository = new SampleRepository()
const sampleService = new SampleService(sampleRepository)

await sampleService.create("piyo")

repository は「Sample を受け取る save メソッド」を持っていれば、 SampleRepository である必要はありません。

よって、 SampleService と SampleRepository の直接的な依存関係はなくなって、必要な依存をコンストラクタから注入できるようになったということになります。

こうすると、単体でテストするときはモックのリポジトリを使用する、といったことを簡単に行えるようになります。

const mockSampleRepository = new MockSampleRepository()
const sampleService = new SampleService(mockSampleRepository)

アブストラクト・ファクトリで DI を活用する

アブストラクト・ファクトリとは、インスタンスを生成するメソッドを定義するデザインパターンです。

例えば、車に関連するクラス(Car)のインスタンスを生成する CarFactory クラスだと。

class CarFactory {
  createWheel() {
    return new NormalWheel()
  }

  createBody() {
    return new NormalBody()
  }

  create() {
    return new Car(
      this->createWheel(),
      this->createBody()
    )
  }
}

これをさっきの DI パターンのコードに当てはめると、次の様なものを作ることができます。

class SampleServiceFactory {
  createSampleRepository() {
    return new SampleRepository()
  }

  createSampleService() {
    const repository = this.createSampleRepository()
    return new SampleService(repository)    
  }
}

さらに野心的に、アプリケーションで扱うサービスクラスをアブストラクト・ファクトリで扱える様にしましょう。(長くなるので、メソッド名から create を消します。)

class AppServiceFactory {
  sampleRepository() {
    return new SampleRepository()
  }

  sampleService() {
    const repository = this.sampleRepository()
    return new SampleService(repository)    
  }

  sampleForeverService() {
    const repository = this.sampleRepository()
    return new SampleForeverService(repository)      
  }

  sampleController() {
    const sampleService = this.sampleService()
    return new SampleController(sampleService)
  }
}

こうすると、依存しているクラスにさらに依存しているクラスがあったとしても、シンプルにインスタンスの生成ロジックを書くことができる様になります。

また、一部のモジュールをモックしたい場合は、

class MockAppServiceFactory extends AppServiceFactory {
  sampleRepository() {
    return new MockSampleRepository()
  }
}

const factory = new MockAppServiceFactory()
const sampleController = factory.sampleController()

というふうに、モックしたいインスタンスの生成ロジックを上書きするだけで、それに依存するすべてのクラスに対して、モックされたインスタンスを利用したインスタンスが生成される様になります。

すべてのサービスクラスを DI パターンを用いて記述していれば、この様に依存関係を宣言的に記述するだけで、インスタンスの生成ができるようになります。

DIコンテナとは

先ほどのアブストラクト・ファクトリを用いた例では、自分で依存関係を明示的に解決していました。

しかし、アプリケーションが大規模化すると、依存関係の数や階層が深くなり、手動での管理がより難しくなります。

この問題を解決するために、依存関係を自動的に管理するツールを考えてみましょう。

簡単な設計は次のとおりです。

  • コンテナには、factory() であらかじめトークン(依存の名前を表す文字列)とファクトリを登録できます。
  • コンテナに登録している依存は、get() にトークンを渡すことでインスタンス化して取得することができます。
class DIContainer {
  constructor() {
    this.factories = {};
  }

  factory(name, factory) {
    this.factories[name] = factory;
  }

  get(name) {
    if (this.dependencies[name]) {
      throw new Error(`'${name}' は登録されていません。`);
    }

    return this.dependencies[name](this);
  }
}

// セットアップ
const container = new DIContainer();

// 依存関係の登録
container.factory('SampleRepository', () => {
  return new SampleRepository();
});
container.factory('SampleService', (c) => {
  const repository = c.get('SampleRepository')
  return new SampleService(repository);
});

// 依存関係の解決とインスタンスの取得
const sampleService = container.get('SampleService');
await sampleService.create("piyo");

これだと、あまりアブストラクト・ファクトリを用いた実装と変わらないので、より宣言的に書ける様に次の仕様を追加しましょう。

  • register() にトークン、クラス、依存するクラスのトークンを指定すると、登録したクラスに依存するオブジェクトを注入するファクトリを自動生成して登録できます。
class DIContainer {
  constructor() {
    this.factories = {};
  }

  factory(name, factory) {
    this.factories[name] = factory;
  }

  register(name, Class, dependencies = []) {
    this.factory(name, (container) => {
      const deps = dependencies.map(dep => container.get(dep));
      return new Class(...deps);
    });
  }

  get(name) {
    if (!this.factories[name]) {
      throw new Error(`'${name}' は登録されていません。`);
    }

    return this.factories[name](this);
  }
}

// セットアップ
const container = new DIContainer();

// 依存関係の登録
container.register('SampleRepository', SampleRepository);
container.register('SampleService', SampleService, ['SampleRepository']);

// 依存関係の解決とインスタンスの取得
const sampleService = container.get('SampleService');
await sampleService.create("piyo");

かなりシンプルに書ける様になりましたね。

もう既に、クラス名で察しがついていると思いますが、これがDIコンテナです。

ここで実装しているのは、かなりシンプルなものですが、フレームワークに付属していたり、オープンソースライブラリとして開発されているものは、より多くの機能を持っています。

まとめ

DI(依存性の注入)とは、クラスが必要とするオブジェクトや値を外部から渡す方法です。

これにより、クラス同士の結びつきを弱め、コードの修正やテストがしやすくなります。
たとえば、データベースにアクセスするクラスを直接使うのではなく、そのクラスを外部から渡すことで、テスト時にモックに差し替えることができます。

ファクトリパターンやDIコンテナを使うと、こうした依存関係の管理がもっと楽になります。

特に大きなアプリケーションでは、依存関係が複雑になるので、自動的に管理してくれるDIコンテナが便利です。

初心者の方は、まずクラス内で直接オブジェクトを作成するのではなく、必要なものを外部から渡す設計に挑戦してみてください。これがDIの基本であり、コードの柔軟性と保守性を高める第一歩になります。


コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です