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.ts
にfind
メソッドが実装されていないとエラーになる。
(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>;
}
SampleContoller
はISampleService
に依存している状態。
前述の通り、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.ts
のProviers
に、条件によってインスタンスを紐づけるを変更する処理を追加する。
下の例だと、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)を実装するとテストしやすくなる。
参考