TypeScript でクリーンアーキテクチャ風にアプリを書いてみる

Entity

  • ビジネスルールとなるモデルを定義する
  • DB のスキーマと一致する必要はない。というか DB のことは忘れた方が実装しやすい
  • ストレージに依存しないロジックはここに書く(例えばパスワードの検証などはここに書く)
  • ロジックに対するテストは書く(書いてない)

Repository(interface)

  • データを取得したり永続化したりするためのクラス
  • [MUST] core パッケージではインターフェースとして定義して、実装はしない
  • リポジトリメソッドの戻り値はドメイン層の Entity を使う(Usecase からしか参照されないし)
  • リポジトリメソッドの引数はオブジェクトにして、Entity から Pick, Omit, Partial, Required などの TypeScript utils を使って組み立てる
  • リポジトリクラスも特定のデータストレージに依存しないようする。データストアは RDB かもしれないし、ファイルかもしれないし、外部の API かもしれない気持ちで。
  • この段階のリポジトリ層はインターフェースしかないのでテストはない

Usecase

  • コントローラーなどから呼ばれる処理をユースケースのクラスとして定義する
  • ユースケースはインターフェイスと実装クラスのセットで用意する
  • [MUST] ユースケースクラスで、リポジトリ層のインターフェースやドメイン層のクラスを利用して実装する
  • ユースケースで使うリポジトリは単一とは限らない
  • ユースケースの戻り値はエンティティを利用するが、一応ユースケース層で型を作り直す(コントローラーがドメイン層に依存しすぎるのはよくなさそうなので)
  • ユースケースの引数は、エンティティを利用してユースケース専用のリクエストオブジェクトを提供しておく
  • [MUST] ユースケースクラスはコンストラクタでリポジトリの実装インスタンスを注入する
  • [MUST] ユースケースはインスタンス化されるタイミングではじめてデータストレージが決まるが、実装自体は変化しない
  • テストはテスト用に作ったモックのリポジトリやインメモリのリポジトリを作って、それを利用してテストする。DB は必要ない。このサンプルでは jest mock で完結した。

Repository(Impl)

  • 必要に応じてリポジトリ層を実装する(サンプルでは mock パッケージの Inmemory)
  • 明確に依存を切り離すために、別パッケージとしている
  • [MUST] 各リポジトリメソッドのテストは必要

感想

  • 慣れると TDD や BDD で開発するみたいな、まず振る舞い+α を定義してから、実装していくことができて精神的に楽
  • 振る舞い(UseCase)を実装すると、あとはリポジトリを実際のストレージに合わせて実装するだけなので、シンプル
  • アプリケーションのテストで大変なのは、リクエストからデータストレージ(主に RDB)までを担保したテストになりがちなところなのだけど、クリーンアーキテクチャ だとリポジトリ層の実装クラス以外はデータストレージに依存することはないので、テストが楽になる。