DIコンテナTSyringeで引数のあるコンストラクタを扱う

今頃になってDIコンテナ[TSyringe](https://github.com/microsoft/tsyringe#inject)というものを知った。TypscriptのSyringe(注射器)だからTSyringeという命名らしく、Microsoftが作っているということなので安心感がある。ただ、AngularのDependency Injectionに慣れていたために使い方に混乱したため、いくつか使い方の例を残す。

# Dependency Injection
以下の例では、`Client`クラスが`LogService`クラスに依存(利用)している。Dependency Injectionは`Client`が依存する`LogService`を、`Client`自身で生成したり属性として持たせるのではなく、外部から与える(Injection: 注入)することを指す。これにより`LogService`の実装に依存しすぎなくしたり、それぞれテストしやすくしたりできるらしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/7d350317-18e4-4400-a93c-b08209b378dd.png)
ただ、DIを実現しようとすると、必要になったタイミングでインスタンス化してコンストラクタに与えるという手間がかかる。そこでTSyringeのようなDIコンテナという仕組みによってその手間を減らせるようになっている。

# TSyringeによるDI
注入されるクラスに`@injectable()`デコレータを付けて定義することで、コンストラクタで与えたクラスのインスタンスが与えられるようになる。ここで、Angularの`@Injectable()`デコレータは依存される側に付けるのに対して、TSyringeの`@injectable()`は依存する側に付けるので、同名だけど付ける対象が逆であることに注意する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/bfe919a1-cc09-e9bf-7a97-f3bef00895a4.png)

```typescript
import "reflect-metadata";
import { injectable, container } from "tsyringe";

// Clientが依存する = 注入されるもの
export class LogService {
  private counter = 0;
  constructor() {
    console.log( 'LogService instantiated.');
  }
  log( message: string ) {
    console.log( `(${this.counter++}): ${message}` );
  }
}

@injectable()
export class Client {
  constructor( private service: LogService ) {
    console.log( 'Client instantiated.');
  }
  execute() {
    this.service.log( 'test' );
  }
}

// clientのインスタンス化をDIコンテナに頼むと、
// DIコンテナが LogService をインスタンス化して与えてくれる。
const client = container.resolve( Client );
const client2 = container.resolve( Client );
client.execute();
client.execute();
client2.execute();
client2.execute();
```
この例を実行すると、`Client`のインスタンス化の前に`LogService`がインスタンス化され、`LogService`の`log`メソッドがコールできていることが分かる。

```
LogService instantiated.
Client instantiated.
LogService instantiated.
Client instantiated.
(0): test
(1): test
(0): test
(1): test
```

## シングルトンの注入
前の例では`Client`を複数インスタンス化すると、`LogService`も複数インスタンス化されている。
これに対して、サービスのように1つだけインスタンス化したい場合は`LogService`側に`@singletgon()`デコレータを付ける。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/844174d6-de49-3ef0-9b5b-92c42fd58783.png)

```typescript
import "reflect-metadata";
import {injectable, singleton } from "tsyringe";
import {container} from "tsyringe";

@singleton()
export class LogService {
  private counter = 0;
  constructor() {
    console.log( 'LogService instantiated.');
  }
  log( message: string ) {
    console.log( `(${this.counter++}): ${message}` );
  }
}
```
`@singleton()`デコレータを付けたことで、`LogService`のインスタンスが1回しか作られなくなる。

```
LogService instantiated.
Client instantiated.
Client instantiated.
(0): test
(1): test
(2): test
(3): test
```

## インターフェースへのシングルトンの注入
ここまでは`Client`は`LogService`に直接依存していたが、特定の実装に依存したくない場合がある。その時は以下のようにインターフェースに依存させることがあるのだが、この場合もう少し工夫が必要となる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/34362238-478a-effa-942c-c0a02f33a28b.png)

```typescript
import "reflect-metadata";
import { inject, injectable, singleton, RegistrationOptions } from "tsyringe";
import {container} from "tsyringe";

// Clientが依存するLoggableインターフェース
interface Loggable {
  log( message: string );
}

// Loggableインターフェースの実装(singletonは不要)
export class LogService implements Loggable {
  private counter = 0;
  constructor() {
    console.log( 'LogService instantiated.');
  }
  log( message: string ) {
    console.log( `(${this.counter++}): ${message}` );
  }
}

// LogServiceをDIコンテナに登録する。
// "Loggable"というトークンに対して、シングルトンを作るよう登録している。
container.registerSingleton( "Loggable", LogService );

@injectable()
export class Client {
  // "Loggable"を注入する
  constructor( @inject( "Loggable" ) private service: Loggable ) {
    console.log( 'Client instantiated.');
  }
  execute() {
    this.service.log( 'test' );
  }
}

// clientのインスタンス化をDIコンテナに頼むと、
// DIコンテナが LogService をインスタンス化して与えてくれる。
const client = container.resolve( Client );
const client2 = container.resolve( Client );
client.execute();
client.execute();
client2.execute();
client2.execute();
```
依存される側は、DIコンテナ`container`に登録する。`InjectionToken`(何を注入するかの指定に使う)と、注入するクラスを`registerSingleton`に与えて登録することで、トークン"Loggable"に対して`Loggable`クラスを注入できるようになる。シングルトンでなく通常のインスタンスで良い場合は、`register`で登録する。

依存する側は、コンストラクタで注入したいパラメータに`@inject("token")`デコレータを付ける。ここでは、事前に登録したトークンLoggableを指定することで、`LogService`が注入できるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/d8916a30-9208-aea8-804d-d1f4faed6629.png)

## コンストラクタにパラメータがあるシングルトンの注入
コンストラクタにパラメータがある場合、`container.register( "token", ClassName )`のように登録することはできない。代わりにファクトリという、インスタンスを生成する関数を登録しその内部でパラメータを与えてインスタンス化する。

ただし、シングルトンは直接ファクトリを登録することができない仕様らしいので、まず[1]`register`でファクトリを登録した上で、[2]`registerSingleton`でファクトリのトークンと関連付ける。

```typescript
// Loggableインターフェースの実装(singletonは不要)
export class LogService implements Loggable {
  private counter = 0;
  // パラメータが必要なコンストラクタ
  constructor( private header: string ) {
    console.log( 'LogService instantiated.');
  }
  log( message: string ) {
    console.log( `${this.header} > (${this.counter++}): ${message}` );
  }
}

// [1] ファクトリをトークンLoggableFactoryで登録する。
// ファクトリではDIコンテナが引数に与えられるが、
// 単純にパラメータのあるコンストラクタを呼ぶだけならDIコンテナは使わない。
container.register<Loggable>( "LoggableFactory", { useFactory: (diContainer) => new LogService( "Param" ) } );

// [2] トークンLoggableに対して、先に登録したトークンLoggableFactoryを対応付けて登録する。
container.registerSingleton( "Loggable", "LoggableFactory" );
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/83be9b42-885c-59e0-54da-fe0e7ee2096a.png)
この準備ができてから以下を実行すると、

```typescript
const client = container.resolve( Client );
const client2 = container.resolve( Client );
client.execute();
client.execute();
client2.execute();
client2.execute();
```
結果は以下のようになり、ファクトリで与えたパラメータがヘッダとして使われることが分かる。

```
LogService instantiated.
Client instantiated.
Client instantiated.
Param > (0): test
Param > (1): test
Param > (2): test
Param > (3): test
```

# まとめ
依存する側Dependsクラスに`@injectable`を付ける。インターフェースに注入する場合は、`@inject("token")`で、何を注入するかを明示的に指定する。

依存される側Dependedクラスは、コンストラクタのパラメータの有無、注入先がクラス or インターフェースによって対応が異なる。また、`InjectionToken`には文字列だけでなくコンストラクタそのものが指定できたりするので、TSyringeの説明を参考にする。

基本的にはインターフェースに注入するときは`InjectionToken`には文字列を使うなどして、クラス名とは無関係にした方が良いと思う。

| インスタンス化 | コンストラクタ | 注入先 | 登録 |
| ----         | ----           | ---- | ----  |
| 都度         | パラメータなし | クラス | 不要 |
| 都度         | パラメータなし | インターフェース | `container.register( "token", Depended )` |
| 都度         | パラメータあり | クラス | `container.register( Depended, ファクトリ )` |
| 都度         | パラメータあり | インターフェース | `container.register( "token", ファクトリ )` |
| シングルトン | パラメータなし | クラス | Dependedクラスに`@singleton()`を付ける or `container.registerSingleton( Depended, Depended )` |
| シングルトン | パラメータなし | インターフェース | `container.registerSingleton( "token", Depended )` |
| シングルトン | パラメータあり | クラス |  `container.register<インターフェース>( Depended, ファクトリ )` |
| シングルトン | パラメータあり | インターフェース |  `container.register( "factoryToken", ファクトリ )`と`container.registerSingleton( "token", "factoryToken" )` |