
はじめに
Rust と フレームワーク axum を使って、API サーバーを実装してみました。
対象読者
- Rust で API サーバーを実装したい人
- Rust で DDD を実装したい人
説明しないこと
- Rust の基本的な文法
- DDD の基本的な考え方
- 使っているクレートの使い方
依存の方向
いざ、実装
仕様を決める
今回は、大学がサークルを管理するシステムを作ることにしました。
- メンバーを追加できる
- 4 年生は、追加できない
- メンバーを削除できる
- オーナーは削除できない
- 4 年生は、卒業する
- サークルは最低 3 人以上でないと、活動できない
- サークルは、最大人数が決まっている
- サークルには、代表者が必要
- 20 歳以上の人は、飲み会に参加できる
- 3 年生のみ、サークルの代表者になれる
ドメインレイヤー
サークル集約
まずは、ドメインレイヤーから作成します。
サークル集約は、集約ルートになる Circle と、集約内のメンバーを表す Member の 2 つのエンティティから構成されます。
id には Value Object を使っています。今回、Entity と Value Object の違いなどは、説明しませんので、興味がある方は、調べてみてください。
:::details CircleId
:::
次に、集約に知識をあたえるために、メソッドを実装します。
Rust では impl で struct にメソッドを実装します。
インターフェース
ドメインの振る舞いを外部に公開するためのインターフェースを作成します。
サークル集約を操作するためのインターフェースを作成します。
traitは、他の言語で言うところのinterfaceのようなものです。
インフラストラクチャレイヤー
インフラストラクチャレイヤーは、永続化を担います。
永続化先は問いません。Firestore や Postgres など、なんでも良いです。
今回は、メモリに保存することにしました。
インフラストラクチャレイヤーでは、ドメインレイヤーのインターフェースを実装します。
データベースから取得した値を、XxxDataという名前で表しています。今回は、CircleData と MemberData です。
このデータベースの型をドメインレイヤーの型に変換するために、TryFrom トレイトを実装し、内部で、reconstruct メソッドを使って、ドメインレイヤーのEntityの型に変換しています。
今回は DB が オンメモリなので、あまり意味を感じにくいですが、アプリケーション層との依存をなくすのに有効です。
DB が突然、Firebase に変わっても、データベースの型を変えるだけで、アプリケーション層には影響を与えません。
そのためにも、アプリケーション層がインフラストラクチャレイヤーに依存しないように抽象に依存させることが重要です。(後述します)
DB の実装は DDD にそれほど関係ないので、興味のある方は見てください。
:::details メモリ上に保存する DB の実装
RawLock を使って、排他制御をしています。
:::
アプリケーションレイヤー
アプリケーションレイヤーでは、ユースケースの実現のために、Entity や VO を使って、レポジトリー (インフラ層) に処理を依頼します。
レポジトリーに処理を依頼しますが、インフラ層には依存しません。
いわゆる、依存性逆転の原則を用いて、ユースケースを実現します。
実態ではなく、抽象に依存させるために、トレイトを使います。
今回は、サークルを作成するユースケースを実装します。
ひとつひとつ見ていきます。
-
usecase の io (入出力)
usecase の io を、CreateCircleInputとCreateCircleOutputとして定義します。 -
CreateCircleUsecase 構造体
CreateCircleUsecaseはジェネリクス構造体です。フィールドには、CircleRepositoryInterfaceトレイトを実装したものを受け取ります。 -
impl CreateCircleUsecase
CreateCircleUsecaseの構造体に対して、2つメソッド、newとexecuteを実装します。-
new メソッド
インスタンスを生成するためのメソッドです。CircleRepositoryInterfaceトレイトを実装したものを受け取ります。
他の言語で言うところのコンストラクターです。ここに、依存を注入します。実態ではなく、抽象(トレイト)を設定することで、依存性逆転の原則を実現します。 -
execute メソッド
ユースケースを実行するためのメソッドです。CreateCircleInputを受け取り、Circle Entityを作成し、レポジトリーに保存します。
selfとは、CreateCircleUsecaseの自身を指します。今回はフィールドが1つしかないので、self.circle_repositoryでフィールドにアクセスでき、そのフィールドが、CircleRepositoryInterfaceトレイトを実装していることが保証されているため、createメソッドを呼び出すことができます。
-
これで、ユースケースはドメインのみに依存し、インフラストラクチャレイヤーに依存しないように実装できました。
プレゼンテーションレイヤー
プレゼンテーションレイヤーでは、エンドポイントの定義、リクエストの受け取り、レスポンス、アプリケーション層に渡す値のマッピングを行います。
ひとつひとつ見ていきます。
-
io (入出力)
CreateCircleRequestBodyは、リクエストのボディを受け取るための構造体です。
CreateCircleRequestBodyにfromを実装して、CreateCircleInputを構築できるようにしています。レスポンスも同じような形で実装しています。
-
AppState
AppStateは、アプリケーションの状態を表す構造体です。axumが提供しています。このアプリケーションでは、レポジトリーを保持していて、依存関係を解決する DI コンテナのような役割をしています。
今回は依存が一つしかないので、ありがたみを感じられませんが、例えば、テスト時に、CircleRepositoryをCircleRepositoryMockのようなモックに差し替えることができます。 -
handle_create_circle
handle_create_circle関数は、リクエストを受け取り、CreateCircleUsecaseを実行します。
第一引数のState(state)ではAppStateの値を取り出せます。
取り出した、値を、CreateCircleUsecaseに注入して、executeメソッドを呼び出します。前述したusecaseを実行している部分です。io はそれぞれ任意の形式に変換しています。
まとめ
Rust axum DDD を使って、API サーバーを実装してみました。Rust はまだまだ、業務での採用が少ないですが、バックエンドをはじめに盛り上がってくると思うので、ぜひ、使ってみてください。また、新しい、ユースケースや、集約を追加していただいたりして、学習の手助けになればと思います。
今回のソースコードは、こちら にあります。 main ブランチは更新しているので、記事執筆時点のソースコードは ver 0.1.0 を参照してください。