🦍

DI(Dependency Injection)のメリットを理解する

2023/01/21に公開約2,600字2件のコメント

DI(Dependency Injection)とは

NestJS や Angular でよく耳にする DI とは依存性の注入のことです。
とドキュメントには書いてありますが、なんのことかわかりません。

// test1.service.ts
@Injectable()
export class Test1Service {
  getName() {
    return { name: "john" };
  }
}
// test1.controller.ts
@Controller("test1")
export class Test1Controller {
  constructor(private readonly test1Service: Test1Service) {}

  @Get()
  getName() {
    return this.test1Service.getName();
  }
}

Nest CLI でプロジェクトを作成したときに生成されるコードはこんな感じになっていると思います。
service での処理を controller で呼び出すことができているくらいにしか思いません。

このサービスは必ずしも、DI しないと使えないわけではなく、通常のクラスなので、インスタント化すれば使えます。

// test1.controller.ts
@Controller("test1")
export class Test1Controller {
  private readonly test1Service = new Test1Service();

  @Get()
  getName() {
    return this.test1Service.getName();
  }
}

DI のメリット

1. インスタンス化が一度だけで済む

クラスをインスタンス化すると、そのサービスを使いたい時に毎回インスタンス化する必要があります。

試しに、サービスのコンストラクターに console.log を入れてみます。

// test1.service.ts
@Injectable()
export class Test1Service {
  constructor() {
    console.log("インスタンス化されました");
  }

  getName() {
    return { name: "john" };
  }
}

DI を使わない場合、service class をインスタンス化するたびに、インスタンス化されました と表示されます。

一方、DI を使う場合、DI された service class は一度だけインスタンス化されます。
app.module.ts の providers に service を登録し、読み込み時に一度だけインスタンス化されます。

// app.module.ts
@Module({
  imports: [],
  controllers: [AppController, Test1Controller],
  providers: [AppService, Test1Service],
})
export class AppModule {}

2. 依存関係の解決が自動で行われる

test1.service.ts の中で、test2.service.ts を呼び出すとします。
そして,test1.controller.tsgetAge()を呼び出すとします。

2-1. DI を使わない場合

// test1.service.ts
@Injectable()
export class Test1Service {
  constructor(private readonly test2Service: Test2Service) {}

  getName() {
    return { name: "john" };
  }

  getAge() {
    return this.test2Service.getAge();
  }
}

// test2.service.ts
@Injectable()
export class Test2Service {
  getAge() {
    return { age: 20 };
  }
}

// test1.controller.ts
@Controller("test1")
export class Test1Controller {
  @Get("age")
  getAge() {
    return new Test1Service(new Test2Service()).getAge();
  }
}

Test1Serviceをインスタンス化して、
Test1Serviceに依存しているTest2Serviceをインスタンス化しています。

これが、増えれば増えるほど、ソースコードが複雑になっていき可読性が落ちます。
またメモリの無駄な使用にもつながります。

2-2. DI を使う場合

// test1.service.ts
省略;
// test2.service.ts
省略;

// test1.controller.ts
@Controller("test1")
export class Test1Controller {
  constructor(private readonly test1Service: Test1Service) {}

  @Get("age")
  getAge() {
    return this.test1Service.getAge();
  }
}

test1.service.ts を constructor に登録するだけで、 test1.service.ts が依存している test2.service.ts への依存関係も解決してくれます。

GitHubで編集を提案

Discussion

  1. インスタンス化が一度だけで済む

こちらはDIの特性とは関係ありません。
ライブラリによっては、必要になった際に都度インスタンスを作り直して生成するという挙動をする場合もあります。

DIのメリットとしては、依存性の逆転や、モックや別実装への差し替えの容易さなどもあります。Javaでもよく使われる技術なので、Java関連の情報にあたって見てもよいかと思います。

  1. インスタンス化が一度だけで済む

はNestJSが依存性注入のスコープをデフォルトでシングルトンにしているだけでDIのメリットとは少し違います

公式ドキュメントにもありますが、下のようにすることでスコープを変更することができます

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
ログインするとコメントできます