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

20240626 追記 

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

https://qiita.com/Nicasdream0/items/920d19864b2790f00280

## そもそもDI(dependency injection)とは

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

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

ポイントは2つ

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

## DI(dependency injection)なしの場合

**SampleController.ts**
``` 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**
```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**
```ts
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**
```ts
export interface ISampleService {
  // 実際のfindの中身は定義しない
  find(id: string): Promise<SampleOutVo>
}
```

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

**SampleService .ts**
```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**
``` 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**
```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)`がよしなにやってくれる

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

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

**SampleModule.ts**
```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**
```ts
@Module({
  controllers: [SampleController],
  providers: [
    {
      provide:  process.env.NODE_ENV === 'test' ? SampleTestService : SampleService,
      useClass: constants.I_SAMPLE_SERVICE,
    },
  ],
})
```

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

```ts
// 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)を実装するとテストしやすくなる。

## 参考

https://thundermiracle.com/blog/2021-09-12-di-4-nestjs/

https://docs.nestjs.com/providers

https://docs.nestjs.com/modules