Sassyブログ

好きなことで暮らしを豊かにするブログ

FoalTSでDIしているサービスクラスを環境毎に出し分ける

始めに

FoalTSというTypeScript製のフルスタックフレームワークがあります。

foalts.org

このFoalTSを出始めて間もない頃に使い始めたのですが、日本ではまだなじみが薄いようです。

そのため、なかなかネットで情報が出てこなくドキュメントやGithub issueを見ながら手探りで触り色々解決しながらアプリケーションを開発しています。

その為、少しでも日本語での検索に引っかかるように今後利用する人が情報を得やすいようにしていきたいなと感じております。

また日本ではExpressを初め、NestJSやFastifyなどのフレームワークが多いように見受けられます。

それでは今回のブログタイトルにある FoalTSでDIしているサービスクラスを環境毎に出し分ける 方法についてポイントを踏まえて解説していきます。

背景

アプリのログ出力にwinstonというライブラリを使用しています。

開発、本番ともにファイルでログを出力するようにしていました。

ログの出力先は環境変数で定義しています。

またDIを使ってインスタンスを変数に渡しています。

最近テストコードを作成したため、テスト実行時にロガーのインスタンスが生成される際にログの出力先が見つからないとエラーが発生しテストに失敗していました。

これはテスト用の環境変数ファイルににログ出力先が設定されていな空なのですが…

当初はエラーを回避するためにテスト環境用の設定ファイルにログ出力先を仕込んで対処していたのですが、テスト実行時にもログファイルが出力されてしまう状況となっていました。

あまり不要なところでログファイルを出力したくないので、これを回避したいと思いテスト時にはモックを使用するように変更しようと思いました。

公式ドキュメントのどこを見たか

FoalTSの十分な情報は公式ドキュメントしかないのでまずは公式ドキュメントを見ます。

始めに見たのは以下の箇所

Services & Dependency Injection | FoalTS

createControlloerというテスト時にコントローラのインスタンスを作成するための関数があります。

createControlloerの第二引数に依存関係のインスタンスを渡すことができ、最初はこのやり方でテスト時にモックのインスタンスを渡してあげるようにしようと思いました。

しかし、なぜか渡しても上手くいかず…

時間も勿体ないので他にやり方が無いか探しました。

そうすると以下の箇所で新しい情報が見つかりました。

Services & Dependency Injection | FoalTS

抽象サービスを定義することで、環境に応じて異なるサービス実装を使用できるみたいです。

この方法を試してみることにしました。

しかし、ドキュメントの内容では初見が参考にして実装するには少々理解しづらい部分があったのでポイントを交えて解説をしていきます。

ハマりポイント

1. 抽象クラスに定義するプロパティの説明がない

まぁ見たらイメージできる名前ではありますが、特にconcreteClassNameはドキュメント見ながら実装すると複数の実装クラスを出し分けたい時にクラス名の切り替えをどのようにすればよいのか最初わかりませんでした…

以下は私が作成しているアプリのコードの一部抜粋です。

logger.service.ts

import { join } from 'path';

export abstract class LoggerService {
  static concreteClassConfigPath = 'logger.driver';
  static concreteClassName = 'ConcreteLoggerService';
  static defaultConcreteClassPath = join(__dirname, './consoleLogger.service');

  abstract debug(msg: string): void;
  ・・・省略
}

LoggerService は抽象クラスです。

開発、本番ではファイルにログ出力を行うFileLoggerServiceを使います。

fileLogger.service.ts

export class FileLoggerService extends LoggerService {
  debug(msg: string) {
    ・・・省略
  }
}

export { FileLoggerService as ConcreteLoggerService };

それに対して、テストではコンソールに出力を行うConsoleLoggerServiceを使います。

consoleLogger.service.ts

export class ConsoleLoggerService extends LoggerService {
  debug(msg: string) {
    ・・・省略
  }
}

export { ConsoleLoggerService as ConcreteLoggerService };

concreteClassConfigPath

実装クラスへのパスを定義します。

この定義は環境毎の設定ファイルに追加します。

ドキュメントにも記載があるので詳細は割愛しますが、development.jsonやproduction.jsonに環境毎に使うパスを追加すればOKです。

concreteClassName

実装クラスの名前を定義します。

ここではConcreteLoggerServiceを代入するようにしています。

なぜConcreteLoggerServiceとするのでしょうか?

次の節で解説します。

defaultConcreteClassPath

concreteClassConfigPathに設定値が無かった場合に使われる実装クラスのパスを指定します。

2. export時のクラス名を揃える

これが一番重要なところです。

fileLogger.service.tsconsoleLogger.service.tsを見て気づいた方がいるかと思います。

exportしているクラス名がどちらもConcreteLoggerServiceとなっています。

ここのクラス名を出し分けたい実装クラスで揃えてあげるのがポイントです。

そして、このクラス名をconcreteClassNameに指定してあげます。

そうするとフレームワーク側がそのクラス名を元に環境別に指定したパッケージへアクセスして、インスタンスを生成してくれます。

foal/service-manager.ts at 26e4c95e9ccc90a2d8b06ec157cf945183c98a45 · FoalTS/foal · GitHub

パス指定が正しくないと以下のようなエラーが出ます。

[CONFIG] /example/valid/path/app/services/hoge.service.ts is not a valid package or file for LoggerService class ConcreteLoggerService not found.

また、実装や設定が間違っていたりすると以下のようなエラーが出ます。

[CONFIG] /example/path/app/services/hoge.service.ts is not a valid package or file for LoggerService class ConcreteLoggerService is not a class.
export { ConsoleLoggerService as ConcreteLoggerService };

上記の記載はドキュメントに記載されていますが、1つの実装クラスの例しかなく、まさかその箇所を揃えておくことという記載が全くなかったので上手く動作させるために数時間費やしました…

しかし、これで問題なく環境毎に出し分けられるようになったので問題への対応は完了です。

最後に

FoalTSはニッチなフレームワークなので商用では中々手が出しにくい部分があるのかなと思います。

しかしTypeScript製のフレームワークなので導入時にTypeScript化みたいな手間が発生しません。

導入開始からTypeScriptの恩恵が受けれるのは良いところではあるかと思います。

なので小さなアプリ開発では素早くTypeScriptで開発できるツールとしては最適だなと考えます。

もう少し日本でも普及されると良いですね。