👨🔧
クリーンアーキテクチャのNodeJSによる実装例
はじめに
Amebaの広告プロダクトチームでバックエンドエンジニアをやっている寺沢です。
メディア事業部の広告横軸組織PTAのアドベントカレンダー6日目の記事となります。
クリーンアーキテクチャをNodeJS(Typescriptによる記述)の実装例を交えて紹介しています。
スライドの補足
クリーンアーキテクチャとは?
- コントローラーからドメインまでの依存の方向性を定めたアーキテクチャ
- 内側のものほどサービス独自のロジックを置くことで、何か変更があったとしても最小限の変更で済む
- 例えば、利用するDBが変更になったとしても中のロジックには影響を与えない
コントローラーとは?
- リクエストパラメータをユースケース層に渡し、その結果をパースしてレスポンスとして返す役割
ログインユーザーのアクセス可否(認可)の処理もコントローラあるいはミドルウェアでやっても良いかもしれません。
DTOとは?
- 外部からのレスポンスを一時的に受け取るオブジェクト
- リクエストパラメータやDBから取得したデータなどを受ける際に使う
- ユースケース層やドメインサービス層で使う前にドメインモデルに変する
- 例えば、リポジトリ層でDBから取得したデータはまずDTOに入れ、ユースケース層に返す前にドメインモデルに変換する
リポジトリとは?
- 外部のサービスとデータをやり取りしたり、受け取ったデータをドメインモデルに変換したりする
サンプルコードについて
ORMを使うと、次のデメリットがあるので、SQLを使っています。
- ORMごとの書き方を覚える必要がある(学習コスト+汎用性に乏しい)
- 柔軟なクエリを作りにくい
UnitOfWork(UoW)とは?
- トランザクション内のドメインロジックをリポジトリに流出させないようにするための仕組み
- トランザクションのBeginやCommit、コネクションなどを抽象化することで、依存方向に違反せずにユースケース層でトランザクションを扱える
ユースケース(サービス)とは?
- ドメインモデルのメソッドやドメインサービス、リポジトリなどを呼び出して処理のフローを制御する
手続き的凝集の関数になることが多いと思います。 - 依存方向的に外側にあるリポジトリを利用する場合、そのリポジトリの実装ではなくインタフェースを利用することで依存方向のルールが崩れないようにしている
- 実装はインタフェースを守ってさえいれば、変更してもリポジトリに影響を与えないので依存関係がない
ドメインサービスとは?
- サービス固有のロジック(ドメインロジック)がある
- 基本一つの関数に一つのタスク
- ユースケース層で処理する必要のある一つ一つのタスクを切り出したもの
- ドメインモデルのメソッドとして定義すると違和感のあるドメインロジック
どういう単位でドメインサービスとして切り出すかは判断が難しいことが多々あります。
サンプルコードについて
トランザクション内からも利用できるように、リポジトリを受け取る関数になっています。
ここで渡すリポジトリはトランザクションの場合、トランザクション用のコネクションを持つことになります。
ドメインモデルとは?
- サービス上重要な概念をモデル化したもの
- ドメインロジックはできる限りドメインモデルのメソッドとして定義できると、再利用性が上がるので推奨される
値オブジェクト
- ドメインモデルの各フィールドをプリミティブ型ではなくクラスとして定義したもの
- 値がどういう役割のものか分かりやすくなったり、値のバリデーションや整形などもメソッドで定義できるので扱いやすくなる
一方で、データの入れ替えやキャストなどが増え、コードが複雑になるトレードオフもあるので、必要に応じて取り入れるのが良いと思います。
同じレイヤー内の参照
- 循環参照が発生しやすくなるため、基本禁止
Typescriptではimportの循環参照でエラーは出ないですが、循環実行で無限ループになる恐れはありそうです。 - ドメインモデルについてはネストするモデルを子、ネスト先を親として、親専用の子モデルを作ることでドメインモデル層内の参照を許可
このようにすることで、親が子を、子が親をそれぞれネストするということが発生しないので、循環参照が起きなくなります。
最後に
アーキテクチャについてはいろいろな意見があると思うので、
何かちょっとでも思うことがあればコメントいただけると幸いです。
お読みいただきありがとうございました。
Discussion