2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dependency Injection (依存性の注入) について改めて調べてみた。

Last updated at Posted at 2024-06-26

初めに

前自分で書いたDIの記事を見て、いろいろと説明が足りてないと思うところがあったので、
改めて記事を書いてみました。

対象読者

  • DIの基本概念を理解したいエンジニア
  • 設計パターンとしてDIを使うことを検討しているエンジニア

まず結論

DI(Dependency Injection)とはコードの一部が他のコードに依存している場合、その依存関係を外部から注入するデザインパターン。

  • メリット
    • コードの柔軟性が向上し、変更やテストがしやすくなります。
  • デメリット
    • テストを書かない場合は冗長な実装になります。

Dependencyの管理にかかわる要素

Dependency (依存)にかかわる要素について解説します。

Dependency(依存)

Dependency(依存)とは、あるクラスが他のクラスの機能を必要とする関係を指します。
例えば、クラスAがクラスBのインスタンスを作成して使用している場合、クラスAはクラスBに依存しています。

class Service {
  getMessage() {
    return "Hello, Dependency!";
  }
}

class Client {
  private service: Service;

  constructor() {
    this.service = new Service();
  }

  showMessage() {
    console.log(this.service.getMessage());
  }
}

const client = new Client();
client.showMessage();

ポイント

  • Clientクラスは直接Serviceクラスがないと動きません(ClientServiceの依存関係がある。)
  • Serviceクラスの変更があるとClientクラスにも変更が必要になります。

Dependency Injection(依存性の注入)

Dependency Injectionとは、依存を外部から注入するデザインパターンです。
クラスAがクラスBのインスタンスを外部から受け取る場合、クラスAに依存性(クラスB)を注入したといえます。

class Service {
  getMessage() {
    return "Hello, DI!";
  }
}

class Client {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  showMessage() {
    console.log(this.service.getMessage());
  }
}

const service = new Service();
const client = new Client(service);
client.showMessage();

ポイント

  • Clientクラス内でServiceクラスのインスタンスを作成する必要がありません。
  • Serviceクラス以外をClientクラスで使いたい場合、Clientクラスに渡すインスタンスを変えるだけで済みます。

Dependency Inversion Principle(依存性逆転の原則)

Dependency Inversion PrincipleはSOLID原則の一つで、クラスは別のクラスに依存するべきではなく、両者は抽象(インターフェイス)に依存するべきという考え方です。

インターフェイスとはクラスのメソッドの型のみを定義したものです。
クラスはインターフェイスを継承(implements)して実装されます。

Dependency InjectionとDependency Inversion Principleは併用されることが多いです。

service.ts

interface IService {
  getMessage(): string;
}

class Service implements IService {
  getMessage(): string {
    return "Hello, Dependency Inversion!";
  }
}

client.ts

class Client {
  private service: IService;

  constructor(service: IService) {
    this.service = service;
  }

  showMessage() {
    console.log(this.service.getMessage());
  }
}

const service = new Service();
const client = new Client(service);
client.showMessage();

ポイント

  • ClientクラスはIServiceインターフェイスという抽象的なものに依存し、具体的な実装に依存しないため、変更に強い設計になります。

JavascriptでのDIの実装例

よく使われそうなNest.jsとInversifyJSを例に挙げて説明します。

Nest.jsでのDIの実装

service.module.ts

import { Module } from '@nestjs/common';
import { Service } from './service';

@Module({
  providers: [Service],
  exports: [Service],
})
export class ServiceModule {}

service.ts

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

@Injectable()
export class Service {
  getMessage(): string {
    return "Hello, Nest.js DI!";
  }
}

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { Service } from './service';

@Controller()
export class AppController {
  constructor(private readonly service: Service) {}

  @Get()
  getMessage(): string {
    return this.service.getMessage();
  }
}

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ServiceModule } from './service.module';

@Module({
  imports: [ServiceModule],
  controllers: [AppController],
})
export class AppModule {}
  1. service.module.tsServiceクラスを提供するモジュールを定義しています。
  2. service.tsServiceクラスはメッセージを返すメソッドgetMessageを持っています。
  3. app.controller.tsAppControllerクラスは、Serviceクラスのインスタンスをコンストラクタで受け取り、HTTP GETリクエストに応じてメッセージを返します。
  4. app.module.ts:アプリケーションモジュールを定義し、ServiceModuleAppControllerをインポートしています。

InversifyJSでのDIの実装

inversify.config.ts

import { Container } from 'inversify';
import { Service } from './service';
import { Client } from './client';
import { TYPES } from './types';

const container = new Container();
container.bind<Service>(TYPES.Service).to(Service);
container.bind<Client>(TYPES.Client).to(Client);

export { container };

service.ts

import { injectable } from 'inversify';

@injectable()
export class Service {
  getMessage(): string {
    return "Hello, InversifyJS!";
  }
}

client.ts

import { inject, injectable } from 'inversify';
import { Service } from './service';
import { TYPES } from './types';

@injectable()
export class Client {
  private service: Service;

  constructor(@inject(TYPES.Service) service: Service) {
    this.service = service;
  }

  showMessage() {
    console.log(this.service.getMessage());
  }
}

types.ts

const TYPES = {
  Service: Symbol.for("Service"),
  Client: Symbol.for("Client")
};

export { TYPES };

app.ts

import 'reflect-metadata';
import { container } from './inversify.config';
import { Client } from './client';
import { TYPES } from './types';

const client = container.get<Client>(TYPES.Client);
client.showMessage();
  1. inversify.config.ts:DIコンテナを設定し、ServiceClientクラスをバインドします。
  2. service.tsServiceクラスは@injectableデコレーターを使用してDIコンテナに登録されています。
  3. client.tsClientクラスはServiceクラスのインスタンスをコンストラクタで受け取ります。@injectデコレーターを使用して、Serviceクラスのインスタンスを注入します。
  4. types.ts:DIコンテナで使用する識別子を定義します。
  5. app.ts:DIコンテナからClientクラスのインスタンスを取得し、showMessageメソッドを呼び出します。

DIの活用事例

テストコードにおいてテストクラスを使う

テスト時に依存関係のあるクラスをモック化してInjectionすることで、自身のクラスのメソッドのロジックのテストのみに集中できます。

app.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { Service } from './service';

describe('AppController', () => {
  let appController: AppController;
  let service: Service;

  beforeEach(async () => {
    // モックサービスの作成
    const mockService = {
      getMessage: jest.fn().mockReturnValue('Hello, Mocked DI!'),
    };

    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [
        {
          provide: Service,
          useValue: mockService,
        },
      ],
    }).compile();

    appController = app.get<AppController>(AppController);
    service = app.get<Service>(Service);
  });

  describe('getMessage', () => {
    it('should return "Hello, Mocked DI!"', () => {
      expect(appController.getMessage()).toBe('Hello, Mocked DI!');
    });

    it('should call getMessage method of the service', () => {
      appController.getMessage();
      expect(service.getMessage).toHaveBeenCalled();
    });
  });
});
  1. モックサービスの作成:

    • jest.fn()を使用してServiceクラスのモックを作成し、そのgetMessageメソッドが'Hello, Mocked DI!'を返すように設定しています。
  2. DIコンテナの設定:

    • Test.createTestingModuleを使用してテストモジュールを設定し、AppControllerをコントローラーとして登録しています。
    • Serviceプロバイダーに対してモックサービスをuseValueオプションで提供しています。
  3. 依存関係の注入:

    • app.get<AppController>(AppController)AppControllerのインスタンスを取得し、モックサービスが注入された状態でテストを実行します。
    • 同様に、app.get<Service>(Service)Serviceのインスタンスを取得します。
  4. テストの実行:

    • getMessageメソッドが正しいメッセージを返すかをテストしています。
    • getMessageメソッドが呼び出されたかを確認するテストを追加しています。

環境ごとに依存関係を切り替える

状況によって使用されるサービスクラスを切り替えたいような場面で、依存関係を切り替えることが容易になります。

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { Service } from './service';
import { AlternativeService } from './alternative.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: Service,
      useClass: process.env.USE_ALTERNATIVE_SERVICE ? AlternativeService : Service,
    },
  ],
})
export class AppModule {}

service.ts

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

@Injectable()
export class Service {
  getMessage(): string {
    return 'Hello, Service!';
  }
}

alternative.service.ts

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

@Injectable()
export class AlternativeService {
  getMessage(): string {
    return 'Hello, Alternative Service!';
  }
}

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { Service } from './service';

@Controller()
export class AppController {
  constructor(private readonly service: Service) {}

  @Get()
  getMessage(): string {
    return this.service.getMessage();
  }
}
  1. app.module.ts:

    • 環境変数USE_ALTERNATIVE_SERVICEが設定されている場合、AlternativeServiceを使用し、そうでない場合はServiceを使用します。
    • Serviceプロバイダーに対してuseClassオプションを使用して、実行時に依存関係を切り替えるように設定しています。
  2. service.ts:

    • デフォルトのサービス実装です。getMessageメソッドが'Hello, Service!'を返します。
  3. alternative.service.ts:

    • 代替サービス実装です。getMessageメソッドが'Hello, Alternative Service!'を返します。
  4. app.controller.ts:

    • Serviceクラスのインスタンスをコンストラクタで受け取り、HTTP GETリクエストに応じてメッセージを返します。

まとめ

DIのメリットとデメリットをまとめます。

DIのメリット

依存関係の解決の責務を分離できる

DIを使用することで、依存関係の解決をクラス自身から分離できます。これにより、クラスは自身の主な責務に集中でき、依存関係の管理は外部に委ねられます。

環境による実装の切り替えが楽になる

DIを使用すると、異なる環境や状況に応じて簡単に実装を切り替えることができます。例えば、開発環境ではモックサービス、本番環境では実際のサービスを注入することが可能です。

複雑なテストの実装が楽になる

DIにより、依存関係を簡単にモックに置き換えることができるため、テストの実装が容易になります。これにより、ユニットテストやインテグレーションテストの効率が向上します。

DIのデメリット

テストを実装しな場合、冗長になる

DIを適切に使用しない場合、あまりうま味がありません。
インターフェイスの実装が増えるので、かえって冗長になる可能性があります。

オーバーヘッド

小規模なプロジェクトでは、DIの導入によるオーバーヘッドがパフォーマンスやコードのシンプルさに影響を与えることがあります。

学習コスト

DIパターンやフレームワークの使い方を理解するための学習コストが発生します。特に、新しいメンバーがプロジェクトに参加する際には、その学習が必要になります。

FAQ

よくありそうな質問と回答内容を記載します。

jest.mockとDIによるテストクラスのinjectionの使い分けは

依存関係の多い複雑なクラスのテストではテストクラスのinjectionのほうが適しています。
また、テストクラスは再利用できるので、同じモックを複数のテストで使いまわしたい場合に有用です。

DIを使うべきでない場合はありますか?

小規模なプロジェクトや単純なアプリケーションでは、DIのオーバーヘッドがデメリットになる場合があります。
DIを使用しなくても依存関係が簡単に管理できる場合には、DIの導入は不要かもしれません。

参考資料

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?