はじめに
以前はよくSpringBootを扱っていました。
最近はJavaにふれることがあまりありません。
Java,SpringBootでレイヤードアーキテクチャ、クリーンアーキテクチャを採用した際に、DIはとても便利でした。
そんなこともあり、かなり前ですが、以下のような記事を書いています。
今回は、TypeScriptで便利にDIできそうなライブラリを利用して、その使い勝手を見てみました。
ドキュメントを見ればわかることですが、サクッと実感できることを目指します。
この記事のこと
以下を利用します。
とても簡単に導入でき、DIを実現できました。
前提
DIとはいえ、アプリケーションがないと面白くないので、Honoを用いたAPIを実装し、そこにDIを活用してみます。
準備
Honoアプリケーション
これについては割愛します。
以下を参考に。
サンプルに合わせて以下から始めます。
import { Hono } from 'hono'
const app = new Hono()
app.get('/api/hello', (c) => {
return c.json({
ok: true,
message: 'Hello Hono!',
})
})
export default app
tsyringeのインストール
以下でインストールしましょう。(npmの場合)
npm install --save tsyringe
また、以下とのことで、サンプルに合わせて「reflect-metadata」も導入します。
Add a polyfill for the Reflect API (examples below use reflect-metadata). You can use:
npm install reflect-metadata
DIを使うまえにimportが必要です。
The Reflect polyfill import should only be added once, and before DI is used:
// main.ts
import "reflect-metadata";
いろいろやってみる
準備
こんな適当なclassから始めます。
lass TestClass {
constructor() {
console.log('TestClass のコンストラクタです');
}
greet() {
return 'Hello from TestClass!'
}
}
DIコンテナに登録
以下の通り、@injectableのアノテーションを付与すると、DIコンテナに登録されます。
import {injectable} from "tsyringe";
@injectable()
class TestClass {
constructor() {
console.log('TestClass のコンストラクタです');
}
greet() {
return 'Hello from TestClass!'
}
}
ちなみに起動時に、コンストラクタのconsole.logが出力されます。インスタンスが生成されて、コンストラクタが実行されるようです。(当然っちゃ当然)
取り出す
以下で取り出せます。
一番やりたい、classへのDIはもうちょっと後ろで。
import {container} from "tsyringe";
const testInstance = container.resolve(TestClass);
シングルトン
そのまんま@singletonです。
@singleton()
class TestClass2 {
constructor() {
console.log('TestClass2 のコンストラクタです');
}
greet() {
return 'Hello from TestClass2!'
}
}
@injectableだとシングルトンにならず、取得の都度インスタンスが生成されますが、@singletonだと、一度のみ流れます。
ClassにDI
この辺からやりたいことですね。
こんなinterfaceがあるとして。
// interface
interface InjectSample {
greet: () => string;
}
// 実装
@injectable()
class InjectSampleImpl implements InjectSample {
greet() {
return 'Hello from InjectSampleImpl!';
}
}
DIコンテナへは以下のように登録し。
container.register<InjectSample>("InjectSample", {
useClass: InjectSampleImpl
});
injectする側のclassはこのように実装します。
@injectable()
class Foo {
constructor(@inject("InjectSample") private injectSample: InjectSample) {
console.log(injectSample.greet());
}
}
簡単にDIできますね。便利。
ちょっとだけオブジェクト指向と依存性逆転(実装は適当です)
何も考えないと
こんなリポジトリがあり。
class SampleRepository {
findAll() {
return [
1, 2, 3
];
}
}
usecaseでリポジトリを呼び出し。
class SampleUsecase {
sampleRepository: SampleRepository;
constructor(sampleRepository: SampleRepository) {
this.sampleRepository = sampleRepository;
}
execute() {
return this.sampleRepository.findAll();
}
}
使う時は2段階でインスタンスを作る必要がでちゃう。
const sampleRepository = new SampleRepository();
const sampleUsecase = new SampleUsecase(sampleRepository);
API(など)を実装すると。
こんな感じで、自前で依存関係を準備してあげる必要がある。
app.get("/api/hello2", (c) => {
const sampleRepository = container.resolve(SampleRepository);
const useCase = new SampleUsecase(sampleRepository);
return c.json({
ok: true,
data: useCase.execute(),
});
});
DIを使うと。
interfaceを用意。
interface ISampleUsecase {
execute: () => number[];
}
interface ISampleRepository {
findAll: () => number[];
}
リポジトリを実装し。
class SampleRepository2 implements ISampleRepository {
findAll() {
return [
4, 5, 6
];
}
}
DIコンテナの登録し。
container.register<ISampleRepository>("SampleRepository", {
useClass: SampleRepository2
});
以下のように実装する。
@injectable()
class SampleUsecase2 implements ISampleUsecase {
sampleRepository: ISampleRepository;
constructor(@inject("SampleRepository") sampleRepository: ISampleRepository) {
this.sampleRepository = sampleRepository;
}
execute() {
return this.sampleRepository.findAll();
}
}
APIが以下のような実装になり、依存関係の解決をDIにお任せすることができる。
app.get("/api/hello3", (c) => {
const sampleUsecase2 = container.resolve(SampleUsecase2);
return c.json({
ok: true,
data: sampleUsecase2.execute(),
});
});
SampleUsecase2のインスタンスができた際に、
constructor(@inject("SampleRepository") sampleRepository: ISampleRepository)
でリポジトリが注入されます。
おわりに
やってみた系なので、あまり結論もありませんが。
SpringでできるようなDIも、ある程度便利にTypeScript環境でも行えますね。
TypeScriptにDIを持ち込んで複雑怪奇にするか、という論点はありそうですが。
同じようなことをやってみたい方のご参考になれば幸いです。
以上です。