1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DI(dependency injection)について復習(Nest.js)

Last updated at Posted at 2023-07-31

20240626 追記

DIについて改めて調べてみました

そもそもDI(dependency injection)とは

とても簡潔に言うと、DI(dependency injection)とは、コードの一部が他のコードに依存している場合、その依存関係を外部から注入すること(仕組み)です。これにより、コードはより読みやすく、理解しやすく、テストしやすくなります。

→ っていわれてもわけわかめだと思うので実際にみていく

ポイントは2つ

  • DI(dependency injection)は外部からコードの依存関係を渡してやる。
  • DI(dependency injection)を実装するとテストしやすくなる。

DI(dependency injection)なしの場合

SampleController.ts

import SampleService from 'src/service/SampleService'
import { Controller, Res, Post, Body } from '@nestjs/common';
import { plainToClass } from 'class-transformer';

@Controller('v1/sample')
class SampleController {
    // sampleService`のインスタンスを`new SampleService()`で作成
    private readonly sampleService : SampleService = new SampleService();
    
    @Post()
    async getSamples(@Res() res: Response; @body body: sampleBodyParameter): Promise<SampleResponse> {
        const vo: SampleOutVo = await this.sampleService.find(body.id);
        return plainToClass(SampleResponse,  vo);
    }
}
export default SampleController

SampleService.ts

class SampleService {
    async find(id: string): Promise<SampleOutVo> {
          return { sample:'sample' };
    }
}
export default SampleService

sampleServiceのインスタンスをnew SampleService()で作成している。

当たり前だが、SampleService.tsが存在しないとコードはエラーになる。
(import SampleService from 'src/service/SampleServiceでエラーになる。)

また、SampleService.tsfindメソッドが実装されていないとエラーになる。
(this.sampleService.find(body.id)でエラーになる。)

つまり、SampleContollerの実装にはSampleServiceの先行実装が必要

=「SampleContollerはSampleServiceに依存している」

テスト時にmockデータを返したい場合

以下のようにSampleService.tsにテストのための実装を追加してやる。
テストのためのコードを本番の実装に書かなくてはならないので、処理が乱雑になる。

SampleService

import { Controller, Injectable, Res, Inject, Post, Body } from '@nestjs/common';

@Injectable()
class SampleService implements ISampleService {
    async find(id: string): Promise<SampleOutVo> {
    // テストのためのコードを SampleServiceに書かなくてはならない。
        if (process.env.NODE_ENV == 'test') {
            return { sample: 'testData' };
        } else {
            return { sample: 'sample' };
        }
    }
}
export default SampleService

DI(dependency injection)の場合

前提知識 : interfaceとは

interfaceは、クラスやオブジェクトが持つべきメソッド(関数)やプロパティ(値)の名前と型のみを定義します。実際の中身は定義しません

ISampleService .ts

export interface ISampleService {
  // 実際のfindの中身は定義しない
  find(id: string): Promise<SampleOutVo>
}

中身はinterfaceを継承したクラスで定義する。

SampleService .ts

@Injectable()
// ProjectServiceを継承(implements ISampleService )
class SampleService implements ISampleService {
// ISampleService で定義したfindの中身を定義
    async find(id: string): Promise<SampleOutVo> {
          return { sample:'sample' };
    }
}
export default SampleService

DI(dependency injection)について

SampleController.ts

import SampleService from 'src/service/Interface/ISampleService'
import { Controller, Injectable, Res, Inject, Post, Body } from '@nestjs/common';

@Controller('v1/sample')
class SampleController {
    constructor(
        @Inject(constants.I_SAMPLE_SERVICE)
        private readonly sampleService: ISampleService,
    ) {}
    
    @Post()
    async getSamples(@Res() res: Response; @body body: sampleBodyParameter): Promise<SampleResponse> {
        const vo: SampleOutVo = await this.sampleService.find(body.id);
        return plainToClass(SampleResponse,  vo);
    }
}
export default SampleController

sampleServiceのインスタンスはISampleService型のオブジェクトとして定義されている。
ISampleServiceはインターフェースとして定義されている。

ISampleService.ts

import SampleOutVo from '../vo/sample/SampleOutVo';

export default interface ISampleService {
    find(): Promise<SampleOutVo>;
}

SampleContollerISampleServiceに依存している状態。

前述の通り、ISampleServiceには実際に動く中身が必要ないため
中身を実装しなくてもSampleContollerが動かせる状態が作り出せる

(もちろん、実体がないのでSampleContollerを動かすとsampleService.findでエラーが起きるが)


では、どこでSampleServiceの中身を渡してやるか

@Inject(constants.I_SAMPLE_SERVICE)がよしなにやってくれる

class SampleController {
     // ここ
    constructor(
        @Inject(constants.I_SAMPLE_SERVICE)
        private readonly sampleService: ISampleService,
    ) {}
    

詳しい話は省くが、Module.tsにおいてI_SAMPLE_SERVICEという名前をSampleService
紐づける処理を書いている。

SampleModule.ts

import { Module } from "@nestjs/common";
import SampleController from 'src/controller/SampleController'
import SampleService from 'src/service/SampleService'
import * as constants from './constants';

@Module({
  controllers: [SampleController],
  providers: [
    // ここ
    {
      provide: SampleService,
      useClass: constants.I_SAMPLE_SERVICE,
    },
  ],
})
export class SampleModule {}

このような仕組みを

外部から
コードの依存性=Dependency(ここではSampleService)を
注入=Injection(sampleService: ISampleServiceに注入)

しているように見えるのでDependency Injectionと呼ぶ


テスト時にmockデータを返したい場合

Module.tsProviersに、条件によってインスタンスを紐づけるを変更する処理を追加する。
下の例だと、NODE_ENVという環境変数が'test'の場合、SampleTestService 、それ以外の場合SampleServiceをI_SAMPLE_SERVICEと紐づけている。

SampleServiceProvider.ts

@Module({
  controllers: [SampleController],
  providers: [
    {
      provide:  process.env.NODE_ENV === 'test' ? SampleTestService : SampleService,
      useClass: constants.I_SAMPLE_SERVICE,
    },
  ],
})

テストの際にNODE_ENVにtrueを渡してやると、コードを切り替えることなく、SampleServiceの代わりにSampleTestServiceを呼び出すことができる。

// SampleService
@Injectable()
export default class SampleService implements ISampleService {
    async find(id: string): Promise<SampleOutVo> {
          return { sample: 'testData' };
    }
}

// SampleTestService
@Injectable()
export default class SampleTestService implements ISampleService {
    async find(id: string): Promise<SampleOutVo> {
          return { sample: 'sample' };
    }
}

// SampleController
@Controller()
class SampleController {
    constructor(
        @Inject(constants.I_SAMPLE_SERVICE)
        private readonly sampleService: ISampleService,
    ) {}

    @Post()
    async getSamples(@Res() res: Response, @Body() body: SampleBodyParameter): Promise<SampleResponse> {
        // ここで呼び出されるメソッドはNODE_ENVの値によって変わる
        // trueの場合、SampleTestServiceのgetProject
        // falseの場合、SampleServiceのgetProject
        const vo: SampleOutVo = await this.sampleService.getProject(body.id);
        return plainToClass(SampleResponse, vo);
    }
}


まとめ

  • DI(dependency injection)は外部からコードの依存関係を渡してやる。
  • DI(dependency injection)を実装するとテストしやすくなる。

参考

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?