Laravelの設計で迷っていた。
Laravelで1から作成してみようとした時に、動くものは作れるけど、動くものでしかない。。。
コントローラにビジネスロジックやORMによるDB接続処理をベタ書きしている。。
Serviceクラスを作ってみたが、結局Serviceクラスに依存していてコントローラの記述が少なくなっただけ。。
などなど、同じような状況に陥った方も少なくないのでしょうか?
そんな私が、ここらで最低限これだけは抑えたほうが良いというアーキテクチャを考えてみました。(実際にはかなり凝っているのでいきなりここまでする必要はないと思いますが、わかりにくい部分は省いて作成してみてください!)
ちなみに↓もとても参考になりました。
今回作成したアーキテクチャ
書籍(Book)管理アプリケーション(仮)として作成しています。
app/
├── Domain/:ドメイン層 ビジネスロジックやドメインオブジェクトを含む
│ ├── Entities/:ビジネスエンティティ 例:Book
│ │ └── Book/
│ │ └── Book.php
│ ├── Events/:イベント駆動用(複雑・複数ロジックになった場合使用)
│ │ └── BookAdded.php
│ ├── ValueObjects/:値オブジェクト 例:BookId
│ │ └── BookId.php
│ ├── Exceptions/:例外を管理
├── UseCases/:アプリケーションのビジネスロジックを実行するユースケースインタラクタとそのインターフェース
│ ├── Common/:共通のCRUD処理を記載した抽象クラス
│ │ └── BaseInteractor.php
│ │ └── BaseInteractorInterface.php
│ ├── Book/
│ │ ├── AddBook/
│ │ │ ├── AddBook.php: ユースケースのエントリポイント
│ │ │ ├── Request.php:ユースケースに渡されるリクエストデータ
│ │ │ ├── Response.php:ユースケースから返されるレスポンスデータ
│ │ │ ├── Interactor.php:ビジネスロジックを実装するインタラクタ
│ │ │ └── InteractorInterface.php:インタラクタのインターフェース
│ │ └── GetBook/
│ │ ├── GetBook.php
│ │ ├── Request.php
│ │ ├── Response.php
│ │ ├── Interactor.php
│ │ └── InteractorInterface.php
├── Infrastructure/:インフラストラクチャ層 データアクセスや外部サービスとの統合を扱う
│ ├── Common/
│ │ ├── RepositoryInterface.php:共通のCRUDメソッドをに対するインターフェース
│ ├── Book/
│ │ ├── BookRepositoryInterface.php:RepositoryInterfaceを継承
│ │ ├── Eloquent/
│ │ | └── EloquentBookRepository.php:ORMを使用したリポジトリ実装
│ │ └── PostgreSQL/
│ │ └── PostgreBookRepository.php:生のPostgreSQLクエリを使用したリポジトリ実装
├── Http/
│ ├── Controllers/:コントローラ
│ │ ├── Api/
│ │ └── BookController.php
│ ├── Requests/:HTTPリクエストバリデーション
│ │ ├── Book/
│ │ └── AddBookRequest.php
│ ├── Resources/:HTTPレスポンスのリソース
│ │ ├── Book/
│ │ └── BookResource.php
│ └── Middleware/
└── Services/:外部サービスの実装
│ └── NotificationService.php
├── Providers/:InteractorとRepositoryクラスの依存注入
│ └── BookServiceProvider.php
デザインパターン的にはRepositoryパターンやFactoryパターンを採用しています。EntityクラスはLaravelのModelがあるのでもしかしたら必要ないかもしれないです。ここは実際に作ってみてから考えます。
ざっくり説明すると、
- コントローラ(クライアント側)では各メソッド引数またはメソッド内でInteractorInterfaceのインスタンスを呼び出す。
- ↑はサービスコンテナでDI(依存注入)しているためInteractorの具象クラスが呼ばれる。
- Interactorの具象クラスではRepositoryインターフェースを引数でインスタンス化する。
- ↑もサービスコンテナでDI(依存注入)しているためRepositoryの具象クラスが呼ばれる。
- Repositoryの具象クラスでDB処理。
- InteractorはRequest.phpとResponse.phpにより引数、戻り値を定義。
- コントローラー(API)の引数と戻り値は、Request、Resourceモジュールで整形。
- 外美サービス(認証や外部API連携など)はドメインの外側としてServices以下にビジネスロジックも含めて記述
といった感じです。
意識した点
ドメイン駆動を意識
DDDを意識してEntityやValueObjectsを作成しました。また、イベント駆動用として、Eventsディレクトリも作成しました。
基本的なCRUDを抽象化
これは実際にやってみないとわからない部分もあるのですが、UseCasesやInfrastructure層にCommonディレクリを作成して一覧取得(getAll)、詳細取得(getById)や作成(store)、更新(update)、削除(delete)を共通化しました。(実際にうまくいくかはわかりませんが、、、)
各エンティティ固有の処理があったら、Events/やUseCases/[Entity]/[Method]Entity.php
に記述する形になると想定しています。あくまで冗長化を少しでも無くそうということで、ファイル数が極端に減るわけではないのでこれがいいかどうかは作成するサービスに寄ってきそうです。
サービスコンテナでDI(依存注入)する
依存性を逆転させるために、インターフェースを作成し、そのインターフェースに依存する形でサービスコンテナで具象クラスに対して依存注入しています。
まとめ
まだまだDDD初心者なので、わからないことが多いです。実際に社内のベテランバックエンドエンジニアに軽くレビューをもらったのですが、EntityクラスはなくてもLaravelならいけるかもと指摘をもらいました。
また、単体テストをまだ組み込んでいないので、そこの考慮する必要がありそうです。
今後実際にこのアーキテクチャを元にサービスを作成してみて改善点が出てくると思うので、その都度記事にまとめます!