はじめに
今携わっている案件でLaravelにおけるクリーンアーキテクチャを勉強する必要があったので、その備忘録として学んだことを残します。
4層のアーキテクチャ構成を採用しており、当初なぜアーキテクチャにこだわるかが恥ずかしながらわからなかったので、必要性と各層の役割などを簡単に掘り下げていけたらと思っています。
なぜアーキテクチャが必要なのか
最近企業の受託案件でも先方がどういったアーキテクチャを選定するかを求めてくることもあるとか、さすがになぜ必要かは認識しておかないとエンジニアとしてまずいと思いました。
自分で調べた限り、クリーンアーキテクチャのメリットには共通して可読性・保守性といったワードが多かったです。
責務の切り分けを一定のルールに乗っ取り行うことでクラスやメソッド単位の役割が明確になる。
そういう積み重ねを丁寧にやっていけば、チームに誰かがジョインしたタイミングであっても、コードリーディングもしやすいため稼働が周りやすくなります。
一昔前までは、今のものに比べるとハードウェアのCPUやメモリ数が低く、以下にメモリを使用せずCPUを浪費しないコードを書くかに重きがおかれていたようです。
これは今でも気にすべきことですが、現在においてはアプリを長く運用するために保守性が高く堅牢なアプリケーションに対して需要が高いため、クリーンアーキテクチャに対する知見が求められるのだと思います。
アーキテクチャについて
アーキテクチャの名称云々は分かりませんが、以下4層の構成となっています。
- Controller
- Usecase
- Service
- Repository
各層の役割についてそれぞれまとめていきます。
Repository層
外部APIやORMによるDB操作などをRepository層で行なっています。
複雑なロジックは書かないことを心がけ、何をする実装かを明確にした上で実装しました。
例えば、TaskといったモデルのRepository層でuserIdにひもづくレコードを取得する実装は以下のようになります。
(複雑な記載を避けた結果、逆に不自然な実装例となっていますがご容赦ください)
// クラスやuse文は省略しています
public function fetch(int $id): Task
{
return Task::find($id);
}
また、Taskモデルの1レコードを削除する実装は以下のようになります。
public function delete(int $id): Task
{
return Task::find($id)->delete();
}
実務の中で以下の2点を注意されました。
- 返り値voidは極力避けること
- 誰がみてもわかる名称にすること
それぞれ振り返っていけたらと思います。
返り値voidは極力避ける
何かを作成したりするメソッドでも必要な場面が想定される場合は可能な限り返り値を指定することを指摘されました。
これには大きく2つの意味があると思います。
-
汎用性高いメソッドにするため
実装するメソッドはRepository層であれば他の開発者も使う可能性があります。
実装者の使用場面ではvoid
であっても他の実装者は返り値を求める可能性があります。
後から実装しても良いですが、極力実装するタイミングである程度汎用的なメソッドにするために想定しうる返り値を指定することが大事なのかな、と理解しました。 -
テストが書きやすくなる
これは指摘していただいた方がおっしゃっていていたのですが、単体テストが書きやすくなるとのことでした。書きやすくなるというか、返り値まで想定してテストすることでより堅牢なテスト設計が可能になるという意味だと思います。
テストが落ちなければ基本バグはでない、保守性の高いアプリを作るためにはテスト設計まで頭に入れる必要があるな、と思いました。
誰が見てもわかる名称にすること
例えばTask
というモデルを取得するメソッドを実装する際に私は当初以下のような実装をしました。
public function fetchTask(int $userId): Task
{
return Task::where('user_id', $userId)->get();
}
上の実装対して、「fetchByUserIdとかにしましょう。TaskRepositoryのメソッドであればTaskは不要ですし、現状だと主キーによる取得と勘違いする可能性があります」とレビューいただきました。
こういったメソッド名を見て実装内容を判断できるのと、実装内容を見ないと理解できないのではコードリーディングの負担も変わります。一つ一つの命名規則も他の開発者の負担を考慮しなくては、と思いました。
Service層
サービス層はドメインロジックを記述してほしい、とリードエンジニアの方から言われました。
一方でUsecase層ではアプリケーションロジックを記述してほしい、とのこと。
それぞれどう違うのかがよくわからなかったので、調べてみました。
ドメインロジック
そもそもドメインとは、アプリケーションで扱うビジネス上の問題領域や範囲を指すようです。
例えば、オンラインショップサービスのアプリケーションを例にとると、ドメインは商品、注文、顧客、支払いなどの要素で構成されます。商品という要素のドメインに関するロジックでは以下のような例が挙げられます。
-
在庫管理
商品の在庫数を追跡し、注文があった場合に在庫数を更新するなど
-
価格設定
セールやキャンペーンなどの特別な価格設定や、数量割引、顧客ごとの割引などを管理するロジックなど
-
商品カテゴリ
商品を適切なカテゴリーに分類方法
-
配送と在庫の連携
注文が発生した場合、在庫を減らし、配送の準備を行うロジック
つまり、Service層では上記の例のようなロジックをコードで表現する場所として考えて良さそうです。
棲み分けはアプリケーションロジックを調べてから考えようかと思います。
アプリケーションロジック
これはアプリがが実行する具体的な機能や手続きを指します。
ドメインロジックを含むソフトウェア全体のロジックの一部ですが、ドメインロジックとは異なる側面があります。
以下のようなロジックが例として挙げられます。
-
ユーザー認証と認可
ユーザーがアプリケーションにログインする際の認証や、そのユーザーがどの機能やリソースにアクセスできるかを決定する認可ロジックなど
-
エラーハンドリングや例外処理
アプリケーションがエラー状態になった場合の処理や、予期しない状況に対する例外処理
ドメインロジックはビジネスドメイン全体をカバーし、一方でアプリケーションロジックはアプリ全体のルールやプロセスに関連するロジックを扱うようです。
Usecase層
Usecase層は先述のとおり、アプリケーションロジックを実装する層になります。
私の案件では、以下のような実装をUsecase層で記述しています。
- 認証情報の取得(リポジトリ層経由でAuthUserの取得など)
- DB操作に必要なトランザクションを貼る
などです。現状まで経験不足なので、実装箇所はそんなに広くはないのでご容赦を。。。
考え方としてドメインロジックとして扱いきれないアプリケーション独自のルールなどをUsecaseに切り出すという理解らしいので、何か明確な線引きがあるとかではないらしいですね。
Controller層
フロントからRequestを受け取りUsecaseに渡した結果の返り値をViewに返すという処理に終始しています。
受け取って返すという処理に留めておくことを心がけていますが、viewに返す変数の整形は行なっています。
本来これはViewModel
を使って整形すべきかもしれませんが、実装上の負担と天秤にかけつつ複雑な処理にならなければ、コントローラで実装しています。
最後に
簡単に4層アーキテクチャそれぞれの役割と必要性について触れました。
最後Controller層の箇所は息切れしたけど、一旦ここまでとさせていただきます。
学んだことの簡単なメモ程度な記事ですが、これからも自分の考えをブログにアウトプットしていきます😀