「コード品質?なにそれ。とりあえずコード書いて」というプロジェクトで働くこと数年、最終的にはプロジェクト全体でクラスが密結合しまくった状態になり、どこか修正する度に他の予期しない不具合が起きていました。
ユニットテストも全くなく、修正後の動作テストを全て手動で行っていたため、コード書く時間より動作テストをしている時間の方が長くなっているという状態に疲れ果て「次のプロジェクトでは不具合が起きにくいアーキテクチャでやったるぞー」と決意、しばらくしてその機会が訪れかつテックリードとして参画出来たので以前から名前だけは知っていた Vertical Slice Architecture を試してみたのを記事にしました。
ネット上から得た知識のみなので、本来の Vertical Slice Architecture として正しくない可能性が非常に高いです。
ChatGPT に聞くとドメイン駆動開発(DDD)と関連性があるっぽい感じですが、DDD についてあまり理解していないのもあって全く意識していなかったので、結構な乖離があると思います。
ディレクトリ構成サンプル
app/
├── Shared/
│ ├── LoginUserInterface.php
│ └── LoginUser.php
├── Models/
│ ├── ValueObjects/
│ │ ├── User/
│ │ │ ├── UserId.php
│ │ │ ├── UserName.php
│ │ │ └── UserEmail.php
│ │ └── Wiki/
│ │ ├── WikiId.php
│ │ ├── WikiTitle.php
│ │ └── WikiContent.php
│ ├── User.php
│ └── Wiki.php
├── UseCase/
│ ├── Wiki/
│ │ ├── Index/
│ │ │ ├── WikiIndexController.php
│ │ │ ├── WikiIndexRequest.php
│ │ │ ├── WikiIndexResource.php
│ │ │ ├── Domain/
│ │ │ │ ├── WikiIndexDomain.php
│ │ │ │ └── WikiIndexDomainInterface.php
│ │ │ ├── Dto/
│ │ │ │ ├── WikiIndexDomainDto.php
│ │ │ │ ├── WikiIndexRepositoryDto.php
│ │ │ │ ├── WikiIndexServiceDto.php
│ │ │ │ └── WikiIndexResourceDto.php
│ │ │ ├── Exception/
│ │ │ │ └── WikiNotFoundException.php
│ │ │ ├── Repositories/
│ │ │ │ ├── WikiRepositoryInterface.php
│ │ │ │ └── WikiRepository.php
│ │ │ └── Services/
│ │ │ ├── WikiServiceInterface.php
│ │ │ └── WikiService.php
│ │ ├── Insert/
│ │ ├── Update/
│ │ └── Delete/
│ └── News/
│ ├── Index/
│ ├── Insert/
│ ├── Update/
│ └── Delete/
└── Utility/
├── DateFormatter.php
└── PriceFormatter.php
各ディレクトリ
Shared
各 UseCase が共通で使う可能性が高い機能。
(※ログインユーザーの情報を取得するクラスなど)
Models
Laravel の Model は ActiveRecord パターンを採用していて、Model が Entity としての役割を持ちながら、DB操作も可能なクラスになっています(Eloquent)
CakePHP も触った事があるのですが、 CakePHP の ORM は Table という Repository のような責務を持つクラスと、Enitty クラスが明確に分けられており、非常に良かったです。それをちょっと参考にというか Model クラスは Entity の役割だけを担ってもらおうと考えました。
なのでディレクトリ名は Entities という名称が良いかもと考えていたのですが
Laravel の名称は Model なので、経験の浅いメンバーは混乱するかもなと思い Model のままにしています。
あと Model は UseCase ディレクトリ内に配置するか共通で使うか迷ったのですが、ただでさえクラスが多くなりがちなアーキテクチャで Model まで分離したらクラス数が大変な事になるかも、と思い Shared クラスと同様に UseCase 全体で共通としました。
今思えば「クラス数が増えそうだから」という理由で共通にしてしまったのは良くなかったかなと感じてます。1つの UseCase で使う Model って多くても数クラスくらいなので。
ValueObjects だけ共通にしても良かったかも。
ValueObjects
各 Model (エンティティ)が持つ、ビジネスロジックを持つカラムに対して作成します。
例えば、商品というエンティティに対して販売中、停止中、準備中を制御する、1 = 販売中 0 = 停止中 2 = 準備中 という意味を持つ sale_flg というカラムがあれば、sale_flg を引数とする SaleFlg クラス を作成してそのクラス内にビジネスロジックを記述します。
あと緯度と経度とか単一では意味をなさないカラムに対して、緯度と経度を引数とする ValueObject を作成する、など。
UseCase
機能毎にディレクトリを分けます。
その配下には Domain, Dto, Repository, Service ディレクトリがあって、Controller, Request, Resource と1つの機能で必要なクラスが全て揃うようになっています。
UseCase の配下にあるディレクトリ説明
Domain
処理の全体の流れを制御するクラス。
Repository クラスと Service クラスを扱う。
Dto
Data Transfer Object の略。
Request クラスが持つパラメータを受け取って、Domain クラスに渡したり、Service, Repository クラスに渡す際も。
Exception
その UseCase で発生する例外クラス。
例外が出ない、もしくは Laravel が標準で用意している例外クラスで事足りるのであれば作らなくてもOK。
Repositories
DB アクセスを行うクラス。Eloquent で DB からデータを取って来る以外の事は何もしない。
ビジネスロジックは書かない。Model そのものか Collection クラスを返す。
Services
ビジネスロジックだけを担当するクラス。
Utilities
各 UseCase が共通で使うクラス。
意味合いとしては Shared と一緒ですが、こちらは日付のフォーマッターとか金額の消費税計算とか、そういうアプリケーションのドメイン知識と無関係だけど共通で利用する機能を持つクラスを置くディレクトリです。
その他のクラス
あとは UseCase 毎に Controller クラスと Request クラスが1つずつと Resource クラスが1つ以上。(※レスポンスは単一モデルのみとは限らないので、Resource クラスは複数の場合があります)
ファクトリーパターンを使いたい場合に Factories ディレクトリを入れたりします。
所感
メリット
以前のアーキテクチャである、app 配下に Models, Services, Repositories ディレクトリを配置して各クラスを配置する MVC もどきと比べると格段に不具合発生率が減りました。
UseCase は他の UseCase と完全に切り離されており、UseCase から他の UseCase のクラスを呼び出すことを禁止しているため、仮にバグが発生しても他の機能に伝搬することもなく責務が明確に分かれているため原因の特定・修正が短時間で可能。新機能追加も容易です。
ユニットテストも「書いた事ありませんが何か?」というチームメンバーも居ましたが、アーキテクチャ自体が単一責任原則を守ってコード書きやすいので取っ付きやすくなったように思います。
デメリット
クラス数が格段に増え、単純な処理を書くのも時間が掛かるようになりました。
例えば「単一テーブルのデータを全部取ってきて id と name だけ json で返す」という単純な機能を作る際も、クラス毎の責務とか全無視で Controller に全部書けば恐らく10行前後で完成します。しかし責務で厳密にクラス分けすると Controller から Domain 呼んで、Domain から Service 呼んで、Service から Repository を呼んでDBからデータ持ってきて、Controller まで戻して Resource クラスで整形、という手順を踏まなければならないのに加え、当然テストもクラス毎に書くのでメチャクチャしんどいです。
「簡単な処理はルールを曲げてもOK!」にしてしまおうと何度も思いましたが、今までの経験から例外を作ればそこからメンバーによってルールが曖昧になり割れ窓理論的にコード全体に広がるであろう事は想像出来たので、どんな機能でもディレクトリ構成と責務は守りましょうってやった結果、膨大なクラス量になりました。
まとめ
「コード記述量が増えても構わないので不具合出来るだけ発生させない!!!」をチーム内の共通認識と出来るのであれば、悪くないアーキテクチャだと感じています。(そもそもアーキテクチャとして正しく出来ているのかは分かりませんが)
ただアーキテクチャを厳密に適用している、出来ている企業さんは少ないと個人的には思っているので、まぁ良いかなと。
この記事について異論は認めます。
むしろ「こうした方がより Vertical Slice Architecture やで」というのを教えて下さい。周りに教えてくれる方がいらっしゃらないので…。
参考ページ
https://zenn.dev/saitom_tech/articles/vertical-slice-architecture-poem
https://zenn.dev/shotaro_tsuji/articles/82596f1f66ac0a
https://www.milanjovanovic.tech/blog/vertical-slice-architecture