Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8832

TypeScript での DI について

$
0
0

本記事は、ぷりぷりあぷりけーしょんずアプリ開発担当による、ぷりぷりあぷりけーしょんず Advent Calendar 2019の10日目の記事となります。

背景

マイクロサービスの簡単な勉強として、 RESTful API を Node.js で作成しようと開発を始めました。
また、静的型付にしたかったため、流行りの TypeScript を採用。あと、フレームワークでは王道の Express を使用します。
せっかくなので、クリーンアーキテクチャにも挑戦したいなというのもあり、そのアーキテクチャで設計や実装を始めました。
実装を進めていく上で、いちいち constructornewするのが嫌だったのと、 interface の実態がなんなのかを1つのファイルで完結させたかった(まさに DI Container)というのがあり、なんかいいのがないのか調べたところ、 InversifyJSとなるものを発見。
これはなかなかいいなと感じたため、記事にしてみようかと思った次第どす。

InversifyJS とは

TypeScript での強力で軽量の DI コンテナです。

セットアップ

Node.js でのプロジェクトが作成されている前提で話を進めていきます。
TypeScript の開発環境構築に関しては、以下のコマンドを実行するだけで出来上がるかと思われます。
ただ、Node や npm などのインストール方法に関しては省略します。あと、npx も使える前提で進めます。

$ mkdir<project name>
$ cd<project name>
$ npm init -y$ npm i -D typescript ts-node
$ npx tsc --init

ここまでで、TypeScript の環境はできました。(他にもやることは色々ありますが、、)
注意点としては、InversifyJS は TypeScript のバージョン 2.0 以上をサポートしているため、インストールの際はバージョンに気をつけてください。
それでは、InversifyJS の環境構築していきます。
っとは言っても、やり方は README に記載している通りにすれば完了です。笑
まずはプロジェクトのルートで以下コマンドを実行

$ npm i -S inversify reflect-metadata

次に tsconfig.json のコンパイルに関する設定を編集します。

{"compilerOptions":{"target":"es5","lib":["es6"],"types":["reflect-metadata"],"module":"commonjs","moduleResolution":"node","experimentalDecorators":true,"emitDecoratorMetadata":true}}

個人的には、 targetlibに関しては、 esnextでいい気もしますが、そこはお好みで。
あと、相対パスの設定とビルドした際に吐き出されるフォルダ指定の設定も含めたいので、以下の設定を追加します。(ここもお好みで)

{"compilerOptions":{..."emitDecoratorMetadata":true,"paths":{"@/*":["src/*"]},"outDir":"./dist"}}

以上で、セットアップ完了です。

実装

それでは実際に実装に入ってみます。
実装に関してはクリーンアーキテクチャを自分なりに組んでおり、そのソースを記載してく感じになりますが、InversifyJS 雰囲気だけ感じてもらえればなと思います。
今回は1リソースをサンプルに記述していこうかと思います。
内容はポケモン情報一覧取得です。(ポケモンに関する薄っぺらい情報一覧を返すエンドポイントを作成します)

各パーツの作成

まず、データベースからデータ取得する repository を定義してきます。
こちらは、マルチ DB 対応・モックデータ取得といったように、汎用性を効かせるため、interface で定義してきます。
今回使用している ORM では TypeORMってのを用いています。(そちらの説明は主旨とは異なるため省きます)

src/domain/repositories/IPokemonRepository.ts
importPokemonsfrom'@/domain/entities/Pokemons';exportdefaultinterfaceIPokemonRepository{findAll():Promise<Pokemons[]>;}

こちらの interface に対して、どの実態のインスタンスを格納するかを InversifyJS で設定していきます。
その設定の話は後にするとして、まずは実態を実装していきましょう。

src/infrastructure/repositories/PokemonRepository.ts
import{injectable}from'inversify';importIPokemonRepositoryfrom'@/domain/repositories/IPokemonRepository';importPokemonsfrom'@/domain/entities/Pokemons';@injectable()exportdefaultclassPokemonRepositoryimplementsIPokemonRepository{publicasyncfindAll():Promise<Pokemons[]>{returnPokemons.find().catch(err=>{throwerr;});}}

DI に関わるクラスなどには @injectable()を付与します。

次にポケモン一覧取得の Usecase を定義します。

src/usecases/pokemons/ISearchPokemonUsecase.ts
importPokemonSearchResponsefrom'@/usecases/dto/models/PokemonSearchResponse';exportdefaultinterfaceISearchPokemonUsecase{search():Promise<PokemonSearchResponse[]>;}

こちらも、実態を実装していきます。

src/interactores/pokemons/SearchPokemonInteractor.ts
import{injectable,inject}from'inversify';import'reflect-metadata';importISearchPokemonUsecasefrom'@/usecases/pokemons/ISearchPokemonUsecase';importTYPESfrom'@/registories/inversify.types';importIPokemonRepositoryfrom'@/domain/repositories/IPokemonRepository';importPokemonsfrom'@/domain/entities/Pokemons';importPokemonSearchResponsefrom'@/usecases/dto/models/PokemonSearchResponse';@injectable()exportdefaultclassSearchPokemonInteractorimplementsISearchPokemonUsecase{@inject(TYPES.IPokemonRepository)privaterepository:IPokemonRepository;publicasyncsearch():Promise<PokemonSearchResponse[]>{constpokemons:Readonly<Pokemons>[]=awaitthis.repository.findAll();returnpokemons.map((p):PokemonSearchResponse=>newPokemonSearchResponse(p.id,p.code,p.name,p.generationNo));}}

こちらも同様に @injectable()を先頭に付与します。また、先ほどの repository の interface をメンバ変数として定義しています。
こちらには @injectを付与します。その引数に関しては後に定義していきます。
最後に Controller を定義します。

import{Request,Response}from'express';import{injectable,inject}from'inversify';import'reflect-metadata';importTYPESfrom'@/registories/inversify.types';importISearchPokemonUsecasefrom'@/usecases/pokemons/ISearchPokemonUsecase';importPokemonSearchResponsefrom'@/usecases/dto/models/PokemonSearchResponse';importPokemonSearchResponseViewModelfrom'@/usecases/dto/viewModels/PokemonSearchResponseViewModel';@injectable()exportdefaultclassPokemonController{@inject(TYPES.ISearchPokemonUsecase)privateusecase:ISearchPokemonUsecase;asyncsearch(_:Request,res:Response):Promise<void>{constresponse:PokemonSearchResponse[]=awaitthis.usecase.search();constresult:PokemonSearchResponseViewModel[]=response.map((r):PokemonSearchResponseViewModel=>newPokemonSearchResponseViewModel(r.id,r.code,r.name,r.generationNo));res.status(201).json(result);}}

DTO の中身に関しては、Entity とほぼほぼ変わらないクラスとなっています。
まだまだ、ハードコードを直すこと(HTTP statusとか)やトランザクション周りの設定など、やることは多いですがざっとこんな感じで完成です。

DI Container の定義

ソース中に出てきた TYPESや実態をどのように設定しているかについて、記載していきます。
まずは TYPESの定義をしてきます。
ここでは、どのクラスが実態となるのかの識別子を定義しています。定義方法は自由で、クラスでも文字列リテラルでもいいそう。
今回は README にあるような Symbolで定義しています。

src/registories/inversify.types.ts
constTYPES={PokemonController:Symbol.for('PokemonController'),IPokemonRepository:Symbol.for('IPokemonRepository'),ISearchPokemonUsecase:Symbol.for('ISearchPokemonUsecase')}asconst;exportdefaultTYPES;

最後に DI Container の定義です。

import{Container}from'inversify';importIPokemonRepositoryfrom'@/domain/repositories/IPokemonRepository';importPokemonRepositoryfrom'@/infrastructure/repositories/PokemonRepository';importISearchPokemonUsecasefrom'@/usecases/pokemons/ISearchPokemonUsecase';importSearchPokemonInteractorfrom'@/interactores/pokemons/SearchPokemonInteractor';importPokemonControllerfrom'@/controllers/pokemons/PokemonController';importTYPESfrom'@/registories/inversify.types';constcontainer=newContainer();container.bind<IPokemonRepository>(TYPES.IPokemonRepository).to(PokemonRepository);container.bind<ISearchPokemonUsecase>(TYPES.ISearchPokemonUsecase).to(SearchPokemonInteractor).inSingletonScope();container.bind<PokemonController>(TYPES.PokemonController).to(PokemonController);exportdefaultcontainer;

まず、今まで定義した interface とその実態クラス、クラス中に @injectしているクラスをインポートします。
その後に、先ほど定義した TYPESの識別子を用いて、どの interface にはどの実態クラスが格納されるといった設定をしていきます。
今回は普通に設定しましたが、環境変数を参照してこの実態クラスを格納する、といったような設定を記述していくと思われます。

src/registories/inversify.config.ts
const{NODE_ENV}=process.env;if(NODE_ENV==='development'){container.bind<IPokemonRepository>(TYPES.IPokemonRepository).to(PokemonRepository).inSingletonScope();container.bind<ISearchPokemonUsecase>(TYPES.ISearchPokemonUsecase).to(SearchPokemonInteractor).inSingletonScope();container.bind<PokemonController>(TYPES.PokemonController).to(PokemonController).inSingletonScope();}elseif(NODE_ENV==='test'){container.bind<IPokemonRepository>(TYPES.IPokemonRepository).to(PokemonMock).inSingletonScope();container.bind<ISearchPokemonUsecase>(TYPES.ISearchPokemonUsecase).to(SearchPokemonTestInteractor).inSingletonScope();container.bind<PokemonController>(TYPES.PokemonController).to(PokemonController).inSingletonScope();}

すいません。すっごい適当に書いてます。笑
あくまでも一例だと思ってください。
理想は、環境ごとに config ファイルを用意して( inversify.dev.tsinversify.test.tsなど) どれを読み込むかを inversify.config.tsでいい感じにするがいいのかもしれません。

定義した Controller を Express でコールしてみる

ここまで定義したのを Express Router に読み込ませます。

src/app.ts
import*asexpressfrom'express';import{Request,Response}from'express';import'reflect-metadata';importcontainerfrom'@/registories/inversify.config';importPokemonControllerfrom'@/controllers/pokemons/PokemonController';importTYPESfrom'@/registories/inversify.types';constpokemonControllerContainer=container.get<PokemonController>(TYPES.PokemonController);constapp=express();app.get('/',(req:Request,res:Response)=>pokemonControllerContainer.search(req,res));constport=3000;app.listen(port,()=>console.log(`Example app listening on port ${port}!`));

これで config に基づいて DI された実態クラスのメソッドがコールされ、処理が実行されます。
また、エンドポイントでの処理を以下のように記述するとなぜかうまくいきませんでした。。。(ここ、ちょっとハマりました)

app.get('/',pokemonControllerContainer.search);

まとめ

とりあえず、記事まとめるのはとても疲れますね。(後半、結構雑になってるかもしれません。。。)
結構シンプルな実装になるので、キャッチアップも早くできるかと思われます。
個人的に、DI される実態クラスを設定できるのはとてもありがたいので、サーバサイド開発では今後とも使っていこうと思っています。
結構スター数も多いので安心して使えるパッケージです。気になる方は是非使用してみてください!

明日は @MSHR-Decさんの記事となります!


Viewing all articles
Browse latest Browse all 8832

Trending Articles