今頃になってDIコンテナTSyringeというものを知った。TypscriptのSyringe(注射器)だからTSyringeという命名らしく、Microsoftが作っているということなので安心感がある。ただ、AngularのDependency Injectionに慣れていたために使い方に混乱したため、いくつか使い方の例を残す。
Dependency Injection
以下の例では、Client
クラスがLogService
クラスに依存(利用)している。Dependency InjectionはClient
が依存するLogService
を、Client
自身で生成したり属性として持たせるのではなく、外部から与える(Injection: 注入)することを指す。これによりLogService
の実装に依存しすぎなくしたり、それぞれテストしやすくしたりできるらしい。
ただ、DIを実現しようとすると、必要になったタイミングでインスタンス化してコンストラクタに与えるという手間がかかる。そこでTSyringeのようなDIコンテナという仕組みによってその手間を減らせるようになっている。
TSyringeによるDI
注入されるクラスに@injectable()
デコレータを付けて定義することで、コンストラクタで与えたクラスのインスタンスが与えられるようになる。ここで、Angularの@Injectable()
デコレータは依存される側に付けるのに対して、TSyringeの@injectable()
は依存する側に付けるので、同名だけど付ける対象が逆であることに注意する。
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()
デコレータを付ける。
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
に直接依存していたが、特定の実装に依存したくない場合がある。その時は以下のようにインターフェースに依存させることがあるのだが、この場合もう少し工夫が必要となる。
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
が注入できるようになる。
コンストラクタにパラメータがあるシングルトンの注入
コンストラクタにパラメータがある場合、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" );
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" )
|