Ponz Dev Log

ゆるくてマイペースな開発日記

Node.jsアプリでもAngularみたいにDIしながらアプリを作る

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を実現するのに不可欠なライブラリです。

github.com

正体はTypeScriptで作られたIoCコンテナ(=DIコンテナ)のライブラリ。TypeScriptで使うことが推奨されています。

また、最近のTypeScriptの流れなのか、アノテーションを使っています。@Inject@InjectableといったJavaでもお馴染みのやつを使えます。 詳細はGitHubのREADME.mdを読んでいただければと。

実践

完成形

最初に最終形をお見せします。 ファイル・ディレクトリは以下の通りになります。

CLIのエントリーポイントがsrc/generator.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アプリでフレームワークを使うときも同じように書けるはず。