🎉

Vertical Slice Architectureのサンプルコードを分析してみる

2024/02/01に公開

株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。この記事は、Fukuoka.NET dotnet conf 2023での僕の登壇「VSAについて深掘りしてみる」の参考資料として、Vertical Slice Architectureのいくつかのサンプルプログラムがどのように記述されているかをまとめています。

登壇の練習もYoutubeにあげましたので、よろしければご覧ください。

https://www.youtube.com/watch?v=ziIXsi6gT20

弊社の川江さんが、Vertical Slice Architecture に関する .NET Conf 2023 セッションのまとめという記事を書いてくださり、昨年11月の.NET ConfでのVertical Slice Architectureの発表に関してまとめてくださっています。

この記事の要点として、Vertical Slice Architecture のポイントは以下の2つとなります。

  • それぞれの機能ごとにエンドポイントからユースケースまでファイルシステム上で近くに配置する
  • それぞれの機能ごとの実装は一緒である必要はなく、各機能ごとに使用する詳細機能を決定する

この記事では、たくさんのサンプルコードにどのような傾向があるのかをまとめてみたいと思います。

サンプルコードテーブル

まず、サンプルコードの一覧とそれぞれの項目のまとめをテーブルでご覧ください。
今回のコードに関しては、全て C# で.NETのものを選んでいます。興味深いことに、VSAはC#界隈で話題になり、その他の言語ではあまり話題になっていないようです。
○、△、❌を便宜的に書いています。これはこのように書くことにより自然な形で使うことができる機能ではありますが、VSAではそれぞれの機能ごとにアーキテクチャや採用技術を選択できることをよしとしていて、機能要件によっては複雑な機能がない方が良い場合もありますし、複雑にしないことにより、簡潔に記述できるという利点があり、機能的に十分な場合もありますので、要件に応じて機能を採用していただければと思います。

サンプル リンク リポジトリ ドメインモデル内のビジネスロジック バリデーション コマンド+ハンドラー 1ファイル複数クラス 感想
Luke Parker(dotnet conf 登壇者) Github ○Featureの中にあり ❌ないが追加は可能 ○ 属性で記述 ○あり △IResultリターン ○1クラス 少しシンプルなサンプルという感じ
VsaTemplate Github △なし、DbContextを直接 △Serviceに共通コード △ハンドラー内でGuard ○Recordで定義 △Dbの型リターン ○1クラス 全体的にバランスは取れている
VerticalSliceArchitecture Github △なし、DbContextを直接 ❌なし、各ハンドラに記述 ○ハンドラ前に専用クラス ○あり ○DTOリターン △複数 各レイヤで抽象化されていてわかりやすい
booking-microservices Github ○あり ○リードモデルとライトモデルに分かれている ○ハンドラの前にValidatorを別クラスで実装 ○あり ○Input/Outputリターン △複数 例外なども含めてとてもよくできている
dotnet-2022 Github △なし、DbContextを直接 ❌なし ○ハンドラ前に専用クラス ○あり ○Qurey/Respnoseリターン ○1クラス、ルートクラス内にサブクラス シンプルだが、MinimalAPIを使ってよくできている
Dave Callan dotnet X Post △なし、DbContext直接 ❌なし ❌特に記述無し ❌特に指定無し ❌IActionResultリターン △複数 ほぼただのMVC
Code Opinion by Derek Comartin Blog △なし、DbContextを直接 ❌なし ❌特になし ○あり ○DTOリターン △複数 Youtube用のデモ用のシンプルなプロジェクトに感じる
Sekibanサンプル Github ○あり、イベントソーシング ○値オブジェクトと集約 ○属性で定義、ハンドラ前に実行 ○あり ○Query/Responseリターン ○1クラス、ルートクラス内にサブクラス イベントソーシングの実装を記述することにより、VSA的に実装される

各項目の説明と例

以下で表の各項目のそれぞれに関して、メリット、デメリットを書いていきます。アーキテクチャの選択に関してはどの選択に関しても必ず良い点と悪い点があり、それぞれのケースにおいて、色々な決定が考えられます。その点で常に私たちの選択は "It Depends" - "場合による" という決断となります。ですので各チームがそれぞれの置かれている状況における決定を行っていき、場合によっては短くシンプルなコードで運用を始めて、後に複雑な機能をとりいれていくなどの選択がうまくいくことも大いに考えられます。

リポジトリの抽象化とDbContextの利用

サンプルコードではリポジトリ(データベースへの入出力)をインターフェースで管理しているものと、DbContextをそのまま使用しているものとに分かれます。それらのどちらにも良し悪しがあります。

リポジトリの抽象化の特徴

○ データアクセスロジックを一箇所にまとめることで、コードの可読性と保守性が向上します。
○ データストアが変更された場合でも、リポジトリのインターフェースが変わらなければ、ビジネスロジックのコードを変更する必要はありません。
○ テストが容易になります。モックオブジェクトを使用してリポジトリのインターフェースを実装すれば、データストアに依存せずにビジネスロジックをテストできます。
❌ リポジトリがシンプルなCRUD機能のみの場合、冗長な詰め替えのコードが多くなります。
❌ リポジトリのインターフェースが多くのメソッドを持つようになると、そのインターフェースは理解しにくくなります。

DbContextの利用の特徴

○ DbContextを使用すると、LINQとIQueryableを使用してデータをクエリできます。これにより、データアクセスロジックを簡単に実装できます。
○ DbContextを含むメソッド関しても、接続文字列を変えてSQLiteを使うなどをして、簡単に単体テストをすることができます。
❌ DbContextを直接使用すると、ユースケース内にデータアクセスロジックのビジネスロジックを記述することになる場合があります。これは、コードの保守性を低下させます。
❌ DbContextを使用すると、Entity Framework Coreに依存することになります。これは、将来的にデータアクセス技術を変更する際に問題となる可能性があります。

ドメイン内のビジネスロジック

DDD、ドメイン志向開発を行なっていく上で、ドメインにロジックをまとめて記述することは大きな益があります。VSAにおいても、VSAからアクセスするドメインに共通ロジックを記入する、各工程の状態を値オブジェクトの方として定義して、変換を定義する、そしてデータのまとまりを集約として管理して、集約への入出力をきれいに管理するなどを行うことにより、ドメインロジックをまとめることができます。

上記のサンプルはシンプルなものを想定したサンプルですので、ドメイン内のモデリングは簡単なもの、ドメインモデル貧血症気味のものとなっています。ただ、ドメインの型がすでに定義されているものもあり、モデリングを進めることができる形になっているものもあります。

以下のイメージのように、クリーンアーキテクチャでもドメインモデル貧血症とリッチなドメインモデル両方の実装があり得るのと同じことが、Vertical Slice Architecture でもいうことができます。

Vertical Slice Architecture ではDDDができないのでしょうか?
出来ます!VSAは基本的にフォルダのレイアウトによって機能をまとめるというものなので、DDDの手法で書いたコードを同じフォルダにまとめることで、DDDを実現できます。

ドメインモデル

ドメイン内にビジネスロジックを置くことの特徴

○ ドメインモデルがビジネスロジックを含むことで、モデルがビジネスの要求をより正確に表現できます。ロジックが複雑な場合、モデル内にロジックを組み込むことにより効率的にビジネスロジックを定義できます。
○ ビジネスロジックがドメイン内にあると、再利用性が高まります。同じビジネスロジックを必要とする新たな機能が追加された場合、既存のロジックを再利用することが可能です。
○ テストが容易になります。ビジネスロジックがドメイン内にあると、ドメインモデルの単体テストを通じてビジネスロジックを直接テストすることができます。
△ ビジネスロジックのモデリングは単純なデータの読み書きに比べてスキルを必要とするため、なれない開発者には書きにくいという問題があります。
❌ ドメインモデルがビジネスロジックを含むと、そのモデルが他のドメインやサービスとの結合度を高める可能性があります。これは、モデルが他のドメインやサービスに依存するビジネスロジックを含む場合に特に問題となります。

バリデーションの共通化とハンドラー内での記述

バリデーションはユースケースにおいて大事な一面です。特に以下のいくつかの点を意識すると良いバリデーション機能を作ることができます。

  • バリデーションを簡単に記述できるか?
  • バリデーションの実行を書き忘れないためのサポートがあるか?
  • 複数のバリデーションのエラーをクライアントに簡単に返すことができるか?

その点で、バリデーションを共通化して、コマンドハンドラーの実行、クエリーハンドラーの実行前に行うことができるようにすることは有効な方法です。

バリデーションの共通化して、ハンドラー実行前に検証することの特徴

○ バリデーションロジックが一箇所にまとまるため、コードの可読性と保守性が向上します。
○ 複数のプロパティの検証エラーをリストアップして返すことが容易になります。
○ バリデーションの共通化により、テストが容易になります。共通のバリデーションロジックを一度テストすれば、それが他の場所で正しく動作することが保証されます。
❌ 共通のバリデーションロジックが特定のユースケースに完全に適合しない場合があります。この場合、共通のバリデーションロジックを適応させるために追加のコードを書く必要があります。

ハンドラー内でのバリデーションの特徴

○ ハンドラー内でバリデーションを行うと、そのバリデーションが特定のハンドラーに固有であることが明確になります。これにより、そのバリデーションがどこで使用されるべきであるかが明確になります。
❌ 同じバリデーションロジックが複数のハンドラーで繰り返される可能性があります。これはコードの重複を引き起こし、保守性を低下させます。
❌ ハンドラーがバリデーションロジックを含むと、そのハンドラーが複雑になる可能性があります。これは、コードの可読性と保守性を低下させる可能性があります。
❌ Guard的な記述で書くと、複数のバリデーションエラーを返すコードを書きにくくなります。

コマンドおよびクエリパラメータとそのハンドラーの使用

Vertical Slice Architecture のサンプルの多くは、「コマンド、クエリー」の実行を1つのスライスの単位としています。これにより、「CQRS」(Command Query Responsibility Segregation コマンドとクエリの責務の分離)を実現することができます。コマンドとクエリの責務の分離を行う上で大切なのは、コマンドとクエリで別のモデルを使用することのできる仕組みです。

これはイベントソーシングを用いると自然に実現することができます。保存はイベントの形で行いますが、読み取りのためにはリードモデルを定義したり、特定の目的に合わせてイベントの解釈を記述したプロジェクションを作成することができるからです。しかし、RDBを使っているときも、書き込みのモデルにはデータベースのテーブルの方を用いて、読み取りようにはそのために構成したデータベースのクエリやViewを用いることにより、書き込みの型に依存しない型を読み込みに使用することにより、読み込みと書き込みの責務を分離することが可能です。

コマンドとクエリパラメータをクラスとして定義して、そのハンドラーの記述をする方法の特徴

○ コマンドをクラスとして定義することで、コマンドのパラメータや振る舞いを一箇所にまとめることができます。これにより、コードの可読性と保守性が向上します。
○ コマンドハンドラーは、コマンドの実行ロジックをカプセル化します。これにより、コマンドの実行方法を一箇所で管理でき、コードの再利用性が向上します。
○ コマンドとハンドラーのパターンは、CQRS(Command Query Responsibility Segregation)の原則に従っています。これにより、読み取りと書き込みの操作を分離でき、システムのパフォーマンスとスケーラビリティを向上させることができます。
❌ コマンドとハンドラーのパターンは、CQRSの原則に従っていますが、この原則を適用するには、システムの設計と実装における深い理解が必要です。これにより、学習曲線が急になる可能性があります。

ハンドラーの戻り値に関して

各機能の戻り値を定義する際に、コントローラーが用いているような、IActionResult型を用いることは簡単ですが、ハンドラーに規定の型を定義してその型を返すことにより、ハンドラーの機能としてのテストを行いやすく、またクライアントが型を理解して開発を行うことができます。

ハンドラーの戻り値をDTOやコマンドやクエリのパラメーターに紐づいたレスポンスオブジェクトにすることの特徴

○ レスポンスオブジェクトを使用すると、APIの戻り値の形式が一貫性を保つことができます。これにより、APIの使用者は戻り値の形式を予測しやすくなります。
○ レスポンスオブジェクトを使用すると、APIの戻り値の形式を変更する際に、その変更がAPIの全体に影響を与えることを防ぐことができます。
❌ APIの戻り値の形式を動的に変更する必要がある場合に問題となります。

ハンドラーの戻り値をIActionResultのような汎用型にすることの特徴

○ IActionResultを使用すると、APIの戻り値の形式を動的に変更することができます。これにより、APIの戻り値を柔軟に制御することができます。
△ IActionResultを使用すると、APIの戻り値にHTTPステータスコードやヘッダーなどのHTTP特有の情報を含めることができます。これにより、APIの使用者に対して詳細な情報を提供することができます。
❌ IActionResultを使用すると、APIの戻り値の形式が一貫性を欠く可能性があります。これは、APIの使用者が戻り値の形式を予測することを難しくする可能性があります。
❌ IActionResultを使用すると、APIの戻り値の内容を制御するための追加のコードが必要になる可能性があります。これは、開発の労力を増加させる可能性があります。

1ファイルに複数のルートクラスを書くことに関して

いくつかのサンプルは、1つのファイルに複数のルートクラスを書いています。これはC#の一般的な書き方ではないのですが、VSAの近接の法則の点ではわかりやすいものに感じます。こちらに関しても各プロジェクトで選択をすることができますが、以下のメリットやデメリットがあります。

1ファイルに複数のクラスを書くことの特徴

○ 関連するクラスが同じファイルにあると、コードの可読性が向上します。これにより、コードの理解と保守が容易になります。
○ ファイルの数が減るため、プロジェクトの構造がシンプルになります。これは、プロジェクトのナビゲーションを容易にします。
△ ファイルが長くなる可能性があります。これは、コードの可読性を低下させ、特定のクラスを見つけるのが難しくなる可能性があります。
❌ 複数の開発者が同じファイルを同時に編集すると、マージの問題が発生する可能性があります。

ルートクラス内に複数のクラスやレコードを定義することの特徴

○ ルートクラスと密接に関連するクラスやレコードが同じ場所にあるため、コードの可読性と保守性が向上します。
○ ルートクラスのコンテキスト内でのみ使用されるクラスやレコードを隠蔽することができます。これにより、クラスやレコードの使用範囲が明確になり、誤用を防ぐことができます。
△ あまり多く使われているパターンではないので、開発者がなれない場合があります

Vertical Slice Architecture でのコードの不要な重複を防ぐためのテクニック

表には記述していませんが、いくつかのサンプルはVSAでの不要なコードの重複を防ぐために、いくつかの工夫をしています。いかにMinimal APIの使用とイベントドリブンを用いた共通化について書きます。

Minimal APIの使用

上記のサンプルでは、いくつかのサンプルではMinimal APIを使用しています。

○ Minimal APIを使用すると、APIの定義と実装が一箇所にまとまるため、APIの全体像を把握しやすくなります。これにより、APIの設計と開発が効率的になります。
△ Minimal APIを使用すると、APIの定義が簡潔になる一方で、APIの機能が限定的になる可能性があります。これは、複雑なAPIを開発する際に問題となる可能性があります。
❌ Minimal APIを使用すると、APIの定義と実装が一箇所にまとまるため、APIの規模が大きくなると管理が難しくなる可能性があります。これは、APIの保守性を低下させる可能性があります。

Vertical Slice ArchitectureとMinimal APIの親和性

○ Vertical Slice Architecture (VSA)とMinimal APIは、それぞれが持つ特性から親和性が高いと言えます。VSAは機能ごとにコードを分割し、それぞれの機能が独立して開発・保守できるようにするアーキテクチャです。一方、Minimal APIはAPIの定義と実装を簡潔にし、一箇所にまとめることでAPIの開発と保守を容易にします。
○ これらの特性から、VSAとMinimal APIを組み合わせることで、各機能のAPIを独立して、かつ簡潔に開発・保守することが可能になります。これにより、開発効率とコードの可読性・保守性が向上します。

イベント駆動を用いる共通化

○ イベント駆動を使用することで、各スライスに細かな処理をたくさん書くことを防ぎ、コードの重複を防ぐことができます。これは、特定のイベント(例えば、ユーザーが作成されたとき)が発生したときに実行される処理を一箇所にまとめることで実現されます。

○ 例えば、ユーザーが作成されたときにメールを送る機能を考えてみましょう。この機能をイベント駆動で扱うと、ユーザー作成のコードからメール送信のコードを分離することができます。これにより、ユーザー作成のコードはユーザー作成に集中でき、メール送信のコードはメール送信に集中できます。

以下に、この概念を示す疑似コードを示します:

public record UserCreatedEvent(User User);

public class UserCreatedEventHandler(IMailService mailService) : IEventHandler<UserCreatedEvent>
{
    public void Handle(UserCreatedEvent @event)
    {
        _mailService.SendWelcomeEmail(@event.User.Email);
    }
}

このコードでは、UserCreatedEventがユーザーが作成されたときに発生するイベントを表しています。UserCreatedEventHandlerはこのイベントを処理するクラスで、ユーザーが作成されたときにメールを送る機能を実装しています。

イベント駆動を使用することで、各スライスのコードはそのスライスの主要な機能に集中でき、共通の処理はイベントハンドラーにまとめることができます。これにより、コードの重複を防ぎ、コードの可読性と保守性を向上させることができます。

AI実装とVSAの親和性は高い

○ Vertical Slice Architecture (VSA)は、ファイルが近くに配置されているため、AIによる推論が容易になります。これは、AIが推論の基となる情報を取得する際に、関連するコードが同じまたは近接する場所に存在すると、そのコードの文脈を理解しやすくなるためです。

例えば、特定の機能に関連するすべてのコード(エンドポイント、バリデーション、コマンド、コマンドハンドラー、ドメインのコードなど)が同じフォルダ内に存在する場合、AIはその機能の全体像を把握しやすくなります。これにより、AIはより適切なコードを生成する可能性が高まります。

一方、クリーンアーキテクチャでは、関連するコードが多くの異なるフォルダに散らばっている可能性があります。これは、AIが全体の文脈を理解するのを難しくし、結果として生成されるコードの品質を低下させる可能性があります。

したがって、AIを使用してコードを生成する際には、VSAのようなアーキテクチャを採用することを検討すると良いでしょう。これにより、AIはより効率的に推論を行い、より高品質なコードを生成する可能性があります。

まとめ

上記のように、Vertical Slice Architecture とそのサンプルに関して色々みていきました。Vertical Slice Architecture のポイントは以下の2つとなります。

  • それぞれの機能ごとにエンドポイントからユースケースまでファイルシステム上で近くに配置する
  • それぞれの機能ごとの実装は一緒である必要はなく、各機能ごとに使用する詳細機能を決定する

各サンプルは実際に私たちが直面する複雑なシステムを表現したものではなく、比較的シンプルな機能を扱っているため、細かなドメインモデルの設計は各チームがよく考えて行う必要があります。VSAにより、機能ごとのファイルが探しやすく1つにまとまることによる認知負荷の減少と、探す時間の短縮という点を考えても、VSAには大きな可能性があると感じられました。

その上で、それぞれの状況、チームのメンバーや要件に合わせて各機能で採用する技術を状況に合わせて採用していきますが、大事な点として、シンプルな技術を採用するにしても、テストの行いやすさを考えて技術の選択を考えることと必要な機能詳細に関してのテストをしっかり記述していくことが重要です。

私たちがオープンソースとしてリリースしている、イベントソーシング・CQRSフレームワークのSekibanを使用すると、自然とプログラムをVertical Slice Architecture かつドメインモデルをしっかり行なったものとして作成しやすくなります。ぜひ一度ご覧になっていただければと思います。

ジェイテックジャパンブログ

Discussion