AngularではDependency Injection(DI)で自分で定義したServiceを各コンポーネントで使えるようにできます。Javaの世界でもSpring Frameworkを筆頭に、インターフェイスに対してどのような実装を紐づけるかをDIで実現しています。
ただ、、、サーバーサイドやCLIでNode.jsのアプリを作るとなると途端にDIをすることって少ないですよね。TypeScriptで型やインターフェイスを手に入れたのにClassをいちいちnewしたりimportするのは辛い...とても辛い!!
この記事ではInversifyJSを使ってサーバーサイドのNode.jsのアプリでDIを行います。
概要
実現したいこと
- Node.jsアプリでDIを使ってInterfaceと実装クラスを疎結合にする。
- 生のJSではなく、TypeScriptを使ってClassとInterfaceの恩恵を受ける。
- 題材はCLIアプリケーションとする。
- InterfaceへのDIだけでなく、実装クラスのDIについてもやってみる。
題材
最近お仕事で自分でやったことを例とします。題材とシチュエーションは以下の通りです。ちょっとした自動化アプリです。
- Java(Spring Framework)のWebAPIを作っています。
- 入力チェックのテストコードを全項目網羅して正しく入力チェックエラーを返すことを確認したい。
- 手で書いて目Grepで過不足がないか確認するのは効率がよくない&何かしら見落とすリスクがあるので自動生成する。
- この自動生成をCLIをインターフェイスとしたアプリケーションとして実装する。
InversifyJS
今回のように、JavaScript/TypeScriptでDIを実現するのに不可欠なライブラリです。
正体はTypeScriptで作られたIoCコンテナ(=DIコンテナ)のライブラリ。TypeScriptで使うことが推奨されています。
また、最近のTypeScriptの流れなのか、アノテーションを使っています。@Inject
や@Injectable
といったJavaでもお馴染みのやつを使えます。
詳細はGitHubのREADME.mdを読んでいただければと。
実践
完成形
最初に最終形をお見せします。 ファイル・ディレクトリは以下の通りになります。
CLIのエントリーポイントがsrc/generator.ts
です。
各ディレクトリはクリーンアーキテクチャもどきで構成しています。各ディレクトリと格納クラスの役割は以下の通りです。
- controller
- usecase
- business
- ビジネスロジックを入れます。
- ここだけInterfaceを定義せず、実装クラスのみ置いてます。
- repository
- データストアへの入出力を行います。
- ファイルを扱うのか、DBを使うのかといった実装部分は呼び出し側には直接関係ないので、呼び出し側は
RepositoryInterface.ts
に定義したインターフェイスにのみ依存するようにします。
- common
- 共通部品を入れます。
- Interfaceと実装クラスを紐づける
inversify.config.ts
もこのディレクトリに突っ込んでます。
Interfaceを定義する
まずはController、UseCase、RepositoryのInterfaceを定義します。全部書いているとキリがないので一部だけ。
import { CreateScriptRequest } from "./CreateScriptRequest"; import { CreateScriptResponse } from "./CreateScriptResponse"; export interface ICreateScriptUseCase { generate(request: CreateScriptRequest): CreateScriptResponse; }
実装は何も入れていません。どのようにこのgenerate
関数が実現されるかは実装クラスに委ねられます。そして使用する側は実装クラスではなく、Interfaceにのみ依存します。
実装クラスを書く
Interfaceを実装します。前述した通り、JavaのInjectアノテーションが出てきますね。
Interfaceに実装クラスを注入する対象は @Inject()
、他のクラスで注入させたい対象は @Injectable()
を使用すればあとはただのアプリです。
import { ICreateScriptUseCase } from "./UseCaseInterface"; import { CreateScriptRequest } from "./CreateScriptRequest"; import { CreateScriptResponse, CreateScriptResponseDetail } from "./CreateScriptResponse"; import { CaseScriptListBlg } from "../business/CaseScriptListBlg"; import { CaseScriptBlg } from "../business/CaseScriptBlg"; import { injectable, inject } from "inversify"; import { TYPE } from "../common/Types"; /** * スクリプト生成ユースケースの実装クラス */ @injectable() export class CreateScriptInteractor implements ICreateScriptUseCase { private readonly _listBlg: CaseScriptListBlg; private readonly _scriptBlg: CaseScriptBlg; constructor( @inject(TYPE.CaseScriptListBlg) listBlg: CaseScriptListBlg, @inject(TYPE.CaseScriptBlg) scriptBlg: CaseScriptBlg ) { this._listBlg = listBlg; this._scriptBlg = scriptBlg; } /** * スクリプト生成を実行する. */ generate(request: CreateScriptRequest): CreateScriptResponse { // テストケースのリストを生成 const caseList = this._listBlg.createCaseList(request); // ケース名を元にテストケーススクリプトを生成 this._scriptBlg.generate(request, caseList); // レスポンス生成 return this._createResponse(request.getOutputFilePath(), caseList.length); } private _createResponse(path: string, num: number): CreateScriptResponse { let detail = new CreateScriptResponseDetail(); detail.setCaseName(path); detail.setCaseNum(num); return new CreateScriptResponse("OK.", [detail]); } }
実装クラスと紐づける
最後に、inversify.config.ts
にInterfaceと実装クラスの紐付けを定義します。ここで紐付けはこのクラスでしか行いません。
to()
で実装クラスを紐づけるのですが、toSelf()
なる関数があるらしい。(使い方は分からない...)
import "reflect-metadata"; import { Container } from "inversify"; import { TYPE } from "./Types"; import { IBaseController } from "../controller/IBaseController"; import { CreateScriptController } from "../controller/CreateScriptController"; import { ICreateScriptUseCase } from "../usecase/UseCaseInterface"; import { CreateScriptInteractor } from "../usecase/CreateScriptInteractor"; import { CaseScriptBlg } from "../business/CaseScriptBlg"; import { CaseScriptListBlg } from "../business/CaseScriptListBlg"; import { ICaseListRepository, IScriptRepository } from "../repository/RepositoryInterface"; import { CaseListRepository } from "../repository/CaseListRepository"; import { ScriptRepository } from "../repository/ScriptRepository"; const container = new Container(); // Controller container .bind<IBaseController>(TYPE.CreateScriptCtrl) .to(CreateScriptController); // UseCase container .bind<ICreateScriptUseCase>(TYPE.CreateScriptUseCase) .to(CreateScriptInteractor); // Business Logic container.bind<CaseScriptListBlg>(TYPE.CaseScriptListBlg).to(CaseScriptListBlg); container.bind<CaseScriptBlg>(TYPE.CaseScriptBlg).to(CaseScriptBlg); // Repository container.bind<ICaseListRepository>(TYPE.CaseListRepo).to(CaseListRepository); container.bind<IScriptRepository>(TYPE.ScriptRepo).to(ScriptRepository); export { container };
そして使いたいクラスをDIコンテナから取り出せば完成!
import { container } from "./common/inversify.config"; import { IBaseController } from "./controller/IBaseController"; import { TYPE } from "./common/Types"; const controller = container.get<IBaseController>(TYPE.CreateScriptCtrl); // <-- Here!!
まとめ
InversifyJSを使うと、Node.jsのアプリでもDIやるのは結構簡単ですね。 今回はCLIアプリで一からコードを書きましたが、Webアプリでフレームワークを使うときも同じように書けるはず。