LoginSignup
16
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-17

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

Dependency Injection

以下の例では、ClientクラスがLogServiceクラスに依存(利用)している。Dependency InjectionはClientが依存するLogServiceを、Client自身で生成したり属性として持たせるのではなく、外部から与える(Injection: 注入)することを指す。これによりLogServiceの実装に依存しすぎなくしたり、それぞれテストしやすくしたりできるらしい。
image.png
ただ、DIを実現しようとすると、必要になったタイミングでインスタンス化してコンストラクタに与えるという手間がかかる。そこでTSyringeのようなDIコンテナという仕組みによってその手間を減らせるようになっている。

TSyringeによるDI

注入されるクラスに@injectable()デコレータを付けて定義することで、コンストラクタで与えたクラスのインスタンスが与えられるようになる。ここで、Angularの@Injectable()デコレータは依存される側に付けるのに対して、TSyringeの@injectable()は依存する側に付けるので、同名だけど付ける対象が逆であることに注意する。
image.png

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がインスタンス化され、LogServicelogメソッドがコールできていることが分かる。

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

シングルトンの注入

前の例ではClientを複数インスタンス化すると、LogServiceも複数インスタンス化されている。
これに対して、サービスのように1つだけインスタンス化したい場合はLogService側に@singletgon()デコレータを付ける。
image.png

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

インターフェースへのシングルトンの注入

ここまではClientLogServiceに直接依存していたが、特定の実装に依存したくない場合がある。その時は以下のようにインターフェースに依存させることがあるのだが、この場合もう少し工夫が必要となる。
image.png

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

コンストラクタにパラメータがあるシングルトンの注入

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

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

// 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
この準備ができてから以下を実行すると、

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" )
16
5
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
16
5