🍰

Vertical Slice Architecture 〜アプリケーション、上から切るか?横から切るか?〜

2024/03/12に公開

結合は可能な限りなくしたい

プログラミングには、YAGNIやDRY原則など、従うことが推奨されるTipsが色々とあります。
その中の優先順位としては、以下の記事による順序が非常に参考になります。

ここで本記事において重要な点は、結合度というものは優先順位高く解消したいものだということです。

従来のアーキテクチャ

結合の解消などを踏まえたアーキテクチャの変遷としては、スパゲティコード → N層アーキテクチャ → クリーンアーキテクチャ(オニオンアーキテクチャ・ヘキサゴナルアーキテクチャ) といった流れのようです。

クリーンアーキテクチャはこの図が有名なやつですね。
クリーンアーキテクチャのあの図

このアーキテクチャは、以下の主要なメリットから広く使われているようです。

  • 技術的関心を層別に分割(フォルダもこの関心別に分割)
  • 依存関係の流れを一方向にする
  • ドメインモデルにビジネスロジックを閉じ込める

ちなみにですが、技術的な関心の分け方というのは、副作用と純粋なロジックを層で分割していると考えるのが良いようです。

https://twitter.com/sonatard/status/1557196278433710080

さらに補足ですが、クリーンアーキテクチャはあの図のことではないらしいです。

https://twitter.com/t_wada/status/1290499444467589123

オニオンアーキテクチャ系の辛いところ

オニオンアーキテクチャでブログアプリの例を考えると、フォルダ構成としてはこのような形になります(ChatGPTで生成したので適当ですが、本筋は外してないはずなのでそのまま採用)

src/
|-- app.module.ts                 # アプリケーションのルートモジュール
|-- main.ts                       # アプリケーションのエントリポイント
|
|-- domain/                       # ドメインレイヤー (ビジネスロジック)
|   |-- entities/                 # エンティティ (ドメインモデル)
|   |   |-- post.entity.ts
|   |   |-- comment.entity.ts
|   |   `-- user.entity.ts
|   |
|   `-- repositories/             # リポジトリインターフェース
|       |-- post.repository.ts
|       |-- comment.repository.ts
|       `-- user.repository.ts
|
|-- application/                  # アプリケーションレイヤー (アプリケーションサービス)
|   |-- dtos/                     # データ転送オブジェクト
|   |   |-- create-post.dto.ts
|   |   |-- update-post.dto.ts
|   |   `-- ...
|   |
|   |-- services/                 # アプリケーションサービス
|   |   |-- post.service.ts
|   |   |-- comment.service.ts
|   |   `-- user.service.ts
|   |
|   `-- validators/               # 入力検証器
|       |-- post.validator.ts
|       `-- ...
|
|-- infrastructure/               # インフラストラクチャーレイヤー
|   |-- db/                       # データベース関連 (TypeORMなど)
|   |   `-- database.module.ts
|   |
|   |-- repositories/             # リポジトリの実装
|   |   |-- post.repository.impl.ts
|   |   |-- comment.repository.impl.ts
|   |   `-- user.repository.impl.ts
|   |
|   `-- config/                   # アプリケーション設定
|       `-- configuration.ts
|
`-- interface/                    # インターフェースレイヤー (UIやAPIエンドポイント)
    |-- controllers/              # コントローラー
    |   |-- post.controller.ts
    |   |-- comment.controller.ts
    |   `-- user.controller.ts
    |
    |-- middleware/               # ミドルウェア
    |   `-- logger.middleware.ts
    |
    `-- filters/                  # 例外フィルター
        `-- http-exception.filter.ts

フォルダ構成を見るとわかるように、 投稿機能を改修したいだけであっても、色々なフォルダを参照しないといけない という事象が発生します。サービスが大きくなるほど、この開発者体験の悪化が顕著になるというところがオニオンアーキテクチャ系フォルダ構成の辛いところと言えます。

よくよく考えてみると dtosフォルダやrepositoriesフォルダ単位でコードの内容を追いたいことってあるんだっけ? という疑問は確かに湧いてきますね。

Vertical Slice Architectureとは

そういった状況を踏まえて、 Jimmy Bogard により2018年に提唱された考え方が Vertical Slice Architecture となります。技術的な責務視点ではなく、 ユーザ側・機能的な視点でアプリ(フォルダ・ファイル)を分割しようというもの になります。
VSAイメージ図

また、ここでいう機能というのは、APIレベルの細かな粒度と思って良さそうです(以下同記事の図)。

VSAイメージ図

ちなみにですが、Youtubeもあります(見れてないですが)

https://www.youtube.com/watch?v=5kOzZz2vj2o

(補足: 似たような話とかその前からこの話ってある?)

Vertical Slice Architectureと同様の話は、他にもあります。

フロントエンドでは、 Package by Feature と呼ばれ、こちらを推奨する向きが優勢のようです。

バックエンドでも2016年に ユーザー視点凝集 と呼ばれていたり、

Package by Feature を推す記事が2017年に出ていたりもします。

2019年ですが、 技術駆動パッケージング という名前で Package By Layer側を批判するコメントもあります。
https://twitter.com/MinoDriven/status/1148960876201836546

ほんとにこれで大丈夫か?

うまい話ばかりではなく、普通に以下の批判は見かけます。その辺を少し考えて置きたいところです。

1. これって単なるトランザクションスクリプトにならない?
2. 機能単位で分割すると共通で使用したい処理はどうするの?

1についてはこれは単純に スライス内で分割 すればよいかと。Jimmy Bogardさんの元記事でもスライス切ってからリファクタできる旨の記載があります。
(余談ですが、スライス内で特に分割しないままトランザクションスクリプトなアレなコードになった反省があります)

2についてはこのあと詳しく考えて行きたいと思います。

共通部分の処理について

とりあえずChatGPTにでも聞いてみます。

[質問]
vertical slice architectureにおいて、共通処理を行いたい場合の工夫方法は?

[ChatGPT]
Vertical Slice Architectureでは、アプリケーションを独立したスライスに分割し、それぞれが特定の機能やビジネスロジックを担当します。このアーキテクチャスタイルは、機能ごとの分離を促進し、スケーラビリティとメンテナンスのしやすさを向上させます。しかし、共通する処理や機能を各スライスでどのように取り扱うかは、一つの課題です。

共通処理を効率的に扱うための工夫方法には以下のようなものがあります:

1. 共通ライブラリの使用:
共通する処理や機能を共通ライブラリやモジュールに分離し、各スライスからはこれらのライブラリを利用する形で実装します。これにより、コードの重複を避け、再利用性を高めることができます。

2. ミドルウェアの導入:
アプリケーションの共通処理を扱うために、ミドルウェアを利用します。例えば、認証、ロギング、エラーハンドリングなどの処理をミドルウェアとして実装し、各スライスの前後でこれらの処理を適用することができます。

3. DI(依存性注入)の活用:
依存性注入を使用して、共通処理を担当するサービスやコンポーネントをアプリケーションの各部分に注入します。これにより、各スライスは必要な共通サービスを利用でき、その実装からは疎結合に保たれます。

4. ドメイン駆動設計(DDD)の要素の利用:
共通のドメインモデルやバリューオブジェクトを定義し、これらを各スライスで再利用することで、共通のビジネスロジックやルールを効率的に管理します。

5. CQRS(Command Query Responsibility Segregation)の採用:
CQRSを採用することで、読み込みと書き込みの処理を分離し、共通処理をより適切に組織化することができます。これは、特にデータアクセス層での共通処理を扱う場合に有効です。

6. イベント駆動アーキテクチャの統合:
共通処理をイベントとしてモデル化し、イベントバスやメッセージキューを通じて各スライス間で共有することができます。これにより、処理の結合を減らしつつ、アプリケーションのスケーラビリティと反応性を高めることが可能です。

共通処理の扱い方には、アプリケーションの要件や特性に応じて最適なアプローチを選択する必要があります。これらの工夫方法を適切に組み合わせることで、Vertical Slice Architectureを採用したアプリケーションの開発効率と品質を向上させることができます。

まあ大体こんなもんかなあと。ググってもそれほど変わらなそうです。
1, 2, 3については、認証やロギング、例外ハンドリングといった話がメインなので、今回それは考えたいところではないかなと。
4, 5, 6の部分について少し追加で調べる/考えておこうかなと思います。

ドメイン駆動設計(DDD) と CQRS の適用

CQRSは、Comamnd(書き込み系: Create, Update, Delete)と、Query(読み込み系: Read)で処理を分ける 考え方です。
ちなみにCQRSとCQSとの違いはこちらみたいですが、とにもかくにも今回は Command と Queryで処理を分けるというところを今回はポイントとしておいておきます。

Vertical Slice Architecture では機能ごとに分割するため、このCQRSの考え方とも馴染むはずです。

また、 ドメインモデルの有効性については、保存時(Command)で有効 というコメントもあります(逆に集計時(Query)にはトランザクションスクリプトのほうが合うのではという指摘)。

https://twitter.com/Rynk_m/status/1656102239977738242

そのため、CommandとQueryは分けたうえで、少なくともCommand側に対しては、機能をまたいでドメインモデルを共通して利用する ほうが良いのではないかなと思われます。

Vertical Slice Architecture推しのインフルエンサーの方もEntity(ドメインモデル)のスライスは不要と言ってるので、ここは共通で利用するのがおそらくベターなのでしょう。

https://twitter.com/mjovanovictech/status/1700439549086679533

その他、こちらの記事でもドメインモデルを用いたVertical Slice Architecture について触れられています。

ChatGPTでこれを踏まえてフォルダ構成を作ってもらいましたが、概ねこのような形式で良さそうです。
controllerもcreate-post.controller.tsのように分割すべきでは、という議論はアリそうですが、これくらいが十分実用的な気もしますし、まあそこはケースバイケースですかね。
(その他、「postやcommentのentityファイルをuserと同様に全体で共有するか」あたりも要議論かなと)

src/
|-- app.module.ts                 # アプリケーションのルートモジュール
|-- main.ts                       # アプリケーションのエントリポイント
|
|-- slices/                       # 各機能スライスのフォルダ
|   |-- posts/                    # 「投稿」に関する機能スライス
|   |   |-- commands/             # コマンド関連(書き込み操作)
|   |   |   |-- create-post/
|   |   |   |   |-- create-post.command.ts
|   |   |   |   `-- create-post.handler.ts
|   |   |   `-- update-post/
|   |   |       |-- update-post.command.ts
|   |   |       `-- update-post.handler.ts
|   |   |
|   |   |-- queries/              # クエリ関連(読み込み操作)
|   |   |   |-- get-post/
|   |   |   |   |-- get-post.query.ts
|   |   |   |   `-- get-post.handler.ts
|   |   |   `-- get-posts/
|   |   |       |-- get-posts.query.ts
|   |   |       `-- get-posts.handler.ts
|   |   |
|   |   |-- entities/             # エンティティ
|   |   |   `-- post.entity.ts
|   |   |
|   |   |-- posts.module.ts       # スライス固有のモジュール
|   |   `-- posts.controller.ts   # APIエンドポイント(CQRSによりコマンド/クエリ処理をルーティング)
|   |
|   `-- comments/                 # 「コメント」に関する機能スライス
|       |-- commands/             # コマンド関連
|       |   `-- create-comment/
|       |       |-- create-comment.command.ts
|       |       `-- create-comment.handler.ts
|       |
|       |-- queries/              # クエリ関連
|       |   `-- get-comments/
|       |       |-- get-comments.query.ts
|       |       `-- get-comments.handler.ts
|       |
|       |-- entities/             # エンティティ
|       |   `-- comment.entity.ts
|       |
|       |-- comments.module.ts    # スライス固有のモジュール
|       `-- comments.controller.ts# APIエンドポイント
|
`-- shared/                       # 共有リソース
    |-- user/
    |   |-- entities/
    |   |   `-- user.entity.ts
    |   `-- user.service.ts
    |
    |-- utils/
        `-- logger.service.ts

イベント駆動アーキテクチャの統合

例えば、「ユーザが登録されたときに、ユーザにメールを送信する」といったケースを考えるときに、2つのスライス(ユーザ登録スライスとメール送信スライス)を用意し、C#であれば MediatR というライブラリを介することで、異なる2つのスライス間を疎結合に保ったまま通信ができるそうです。

こちらも先の記事で紹介されています。

その他の例

逆にレイヤードアーキテクチャではこのような方法は取らないほうがいいといった話もあるみたいです。

ちなみにですが、この MediatR の開発者が、Vertical Slice Architecture の提唱者 Jimmy Bogard です。

結論

実際どこまで都合よくうまくいくのかは謎ですが(.NET界隈でしか流行っていなさそうなのも懸念)、直感的には十分今後流行る可能性はあるかなと。今後実践例が出てくることに期待ですね。

余談

Clean Architecture と Vertical Slice Architecture を混ぜた、 feature folder Architecture というのもあるらしいです。

よくわからんアプローチも

その他参考

Discussion