はじめに
弊社では、バックエンドにLaravelを使うことが多いです。
Laravelは非常に生産性が高いと考えてます。
しかし、さらに生産性を上げるべく、モジュール構成の検討を進めています。
標準化を進めるにあたって、目的としたのは以下のものです。
- ユニットテストを楽に書きたい
- 外部サービスなどは流用しやすいようにモジュール化
- Webページ(blade)とAPIで同じロジックを共有したい
この記事は、これを目指して、試行錯誤している内容となります。
前提
- Laravelの機動性の高さを活かすべく、過度に凝ったモジュール構成をしない
- コーディング量が大幅に増えない範囲で検討する
- 実装にはInfyOmのLaravelGenelatorを使用する前提とする
(「とりあえず動く」状態からの変更は非常に生産性が高いので)
そうしてできた構成図がこれ
(※説明のため、若干簡略化しています。)
(※あまりUMLに慣れていないので、間違っているかもしれません。)
ディレクトリ構造は以下のようになりました。
|--app
| |--Usecases
| |--Console
| |--Consts
| |--DTOs
| |--Exceptions
| |--Http
| | |--Controllers
| | | |--API
| | | | |--V1
| | |--Middleware
| | |--Requests
| | | |--API
| | | | |--V1
| |--Models
| |--Providers
| |--Repositories
| |--Rules
| |--Services
| |--Usecases
| |--View
|--config
|--database
| |--factories
| |--migrations
| |--seeders
|--public
|--routes
|--storage
各モジュールの説明と実装時のルール
Controller
- ログインユーザーを意識するのはこのControllerまで
- Webサイト用のWebControllerとアプリやWebアプリからのリクエストを受け付けるAPIControllerそれぞれ作成する
- 特殊な場合のを除きTransactionの制御もControllerで行う
- エラーのCatchを行ない、適切なhttpステータスにして返却する
WebController
- Webサイトの場合に実装する
- Bladeを使用してレンダリング後の結果を返却
APIController
- APIの場合に実装する
- JSONを返却
Request
- 基本的にはRouteの単位で作成する
- パラメータがない場合は、作成しない
- バリデーションルールを定義する
Usecase
- 対象のシステムに依存する処理を書く
- Webサイトと、APIで共通化しても良い(むしろ共通化したい)
- ビジネスロジックを記載する
- メソッドは引数以外の情報(セッションやリクエストなど)は使用しない
- メソッドは引数の値以外の要因で結果が変わらないようにする
- UnitTestを作成する
- 検索、詳細データ取得系の返却はPaginatorが望ましい
- 0〜1件を返却するような詳細データ取得でもPaginatorを返却する
- そうすることで、検索条件のみの変更で同じ処理を流用しやすくなる
Repository
- 論理データモデルの単位でClassを作成する
- メソッドは検索方法や更新方法ごとに作成する
- Joinもここで吸収する
Model
- テーブル(物理データモデル)の単位で作成する
Service
- 対象のシステムに依存しない共通処理を書く
- 外部サービスとのインタフェースもServiceで処理する
- 他サービスでも利用できるようにするのが望ましい
- テーブルへのアクセスが必要な場合、そのテーブルも業務に依存しないようにする
目的をどのように達成したのか
ユニットテストを楽に書きたい
Controllerを含むユニットテストは出力がHTMLだったり、JSONだったりして、非常に作成しづらいです。
また、Requestをテストのパラメータとして与えるのも実装に時間がかかります。
RequestとResposeをControllerの責務として、それ以外の処理をUsecaseとして作成することで、ユニットテストを実施しやすくなります。
外部サービスなどは流用しやすいようにモジュール化
Service(および、ServiceProvider)に外部サービスとのインタフェースを実装します。
UsecaseではServiceを使用するのみとして、外部サービスとの結合度を下げます。
結合度が下がることで、再利用しやすくなり、また、外部サービスへの依存度も下がります。
Webページ(blade)とAPIで同じロジックを共有したい
管理画面などbladeで表示する一覧と、APIで取得する一覧は同じ処理で取得できることが多いです。
検索条件や出力の項目が異なるだけのことが多く、これらの違いは、UsecaseのパラメータやControllerでのレスポンスの編集で吸収できます。
検索条件は、「絞り込み」として表現できることが多く、可変パラメータや検索条件のDTOを実装することで、パラメータの差異に対応できます。
(DTOについては、上記の図には記載していません。)
実際やってみて苦労したところ
Usecaseにクエリを書いてしまう
一番苦労したのはRepositoryの扱い、どこまでRepositoryに任せるかという点。
現状の実装の結果としては、ほぼUsecasaeでJOINのクエリを書いてしまったのでRepositoryパターンの良さを活かしきれませんでした。
この部分は今後の課題として対処していきます。
今後の課題
Usecaseには、かなりの数のメソッドを実装することになりますが、このメソッドのルールをあまり決められなかったです。
パラメータのルールを統一できれば、ソースの共通理解ができて、生産性や保守性をさらに上げることができそうです。
あとは、上記のRepositoryをJOINを含むクエリ問題です。
データ取得のパターンは無限に出てくるので、全てRepositoryに実装するのか、Usecaseで実装するのかは、判断が難しいところです。
ここも、判断のルールを策定して、検討時間の短縮を試みたいと考えてます。
最後に
このモジュール構成も、私達の実装技術もまだまだ発展途上です。
詳しい方のアドバイス等をいただけると幸いです。