Laravel・PHP開発

サービスレイヤーの導入とFat Modelの許容:Laravelにおけるビジネスロジック配置の二面性

seoTitle: Laravelビジネスロジック:サービス層とFat Model…

サービスレイヤーの導入とFat Modelの許容:Laravelにおけるビジネスロジック配置の二面性

metaDescription: Laravelのビジネスロジック配置に悩む方へ。サービス層導入とFat Model許容、それぞれの特徴と規模別の現実解を比較します。

Slug: laravel-service-vs-fat-model

サービスレイヤーの導入とFat Modelの許容:Laravelにおけるビジネスロジック配置の二面性

Laravelで新規プロジェクトを始めるとき、フレームワークが用意してくれたディレクトリ構造を眺めながら、誰もが必ず突き当たる疑問があります。それが「ビジネスロジックをどこに書くか」 という、設計の最初の分岐点です。Controllerに書くのか、Modelに置くのか、それとも新たにServiceクラスを立ち上げるのか――個人開発であっても、CRUDが少し複雑になった瞬間に、この判断を迫られます。

この問いには、絶対的な正解が存在しません。Controllerに処理が集中して肥大化する「Fat Controller」、Eloquentモデルにロジックが溜まる「Fat Model」、あるいはServiceクラスへの単なる移し替えで終わる「Fat Service」。いずれも、開発者コミュニティで繰り返し警鐘が鳴らされてきたアンチパターンです。両極端のどちらが正解かではなく、プロジェクトが今立っているフェーズを見極めて取捨選択すること――それが、Laravelでの長期的な開発を楽にするための再現性のあるアプローチだと、私は考えています。

Fat Controllerが招く3つの技術的負債

最初に目を向けたいのは、Laravelプロジェクトで現実的に発生しやすく、しかも後戻りコストが高いFat Controllerの問題です。Route定義から直接__invokeコントローラやstoreアクションにすべての処理を書き続けていると、数ヶ月で一つのメソッドが数百行に達することは珍しくありません。

Laravelコミュニティでは、Fat Controllerがもたらす技術的負債を大きく3つの観点で整理して語られることが多いです。

  • Understandability(理解しやすさの低下) — 一つのメソッドが長くなるほど、新規メンバーや将来の自分がロジックを読み解くのに時間がかかります。
  • Reusability(再利用性の低下) — 同じ計算や整形処理が複数のエンドポイントに重複しがちで、修正漏れがバグを生みます。
  • Changeability(変更時のバグ誘発) — 一部を変更したときの副作用が見えづらく、影響範囲の調査に大きなコストがかかります。

たとえば「ユーザーが記事を投稿したら、ポイントを付与し、通知を投げ、管理者ダッシュボードの集計も更新する」という一連の処理が、一つのControllerメソッドに連なっているとします。日付や仕様の変更があるたびにそのメソッドを開き、影響範囲を読み解きながら修正していく作業が積み重なると、知らず知らずのうちに開発プロセス全体の再現性を損なっていきます。気がつけば「あのController、誰も触りたがらない」という状態ができあがっている――そんな現場を、コードレビューで何度も見てきました。

Controllerは「受付係」と心得るのが理想です。HTTPリクエストを受けて、適切な仲間にバトンを渡す場所――それ以上の役割を持たせない設計が、後々のあなたを助けます。

MVCの本来の「Model」と、Fat Modelの功罪

それでは、Controllerに処理を置いておくのは望ましくないとして、ではModelに書くのが正解でしょうか。そもそもMVCの「Model」はデータベースのテーブルそのものではなく、ビジネスロジック全般を担う存在を指します。計算、加工、バリデーションの補助、ドメインルールの表現まで含めて担うのが本来の姿で、LaravelのドキュメントでもModelの責務が単なるデータアクセスに限らないことは繰り返し説明されてきました。

実際、Eloquentモデルはリレーションやアクセサ、ミューテタ、スコープといった仕組みを備えていて、小〜中規模のプロジェクトであればEloquentの表現力を活かしてビジネスロジックをModelに集約する「スキニーコントローラー・ファットモデル」 という構成が、少ないボイラープレートで素直に動く選択肢になります。Controller側は驚くほど薄くなり、記述量も減るケースが多いです。Eloquentの力でシンプルに完結するのなら、それが最も費用対効果の高い構成だと言えるでしょう。

ただし、Fat Modelにも当然ながら落とし穴があります。一つのモデルに、計算ロジック、整形処理、複雑なリレーション活用、外部API呼び出し、バリデーションの重複実装などが詰め込まれると、今度はモデルの責務が肥大化し、変更時の影響範囲が読みづらくなります。アカウント関連の処理が膨れ上がったUserモデルや、注文まわりのロジックが数百行に達したOrderモデルを、一度は目にしたことがある方も多いのではないでしょうか。

つまりFat Modelは、条件付きで有効な設計であり、万能薬ではない ということです。Eloquentの便利さを享受できるうちは良いのですが、ドメインが複雑化し始めたら、その時点で別レイヤーへの切り出しを視野に入れる――この「やめどき」を決めておくのが、後々の自分を助けることになります。

サービス層導入の罠 ―形骸化とFat Service化を避ける

ここまでの流れで「それならServiceクラスを作ればいいのでは」と考えるのは自然ですし、それ自体を否定するつもりはありません。Serviceレイヤーを導入してControllerとModelの間に明確な責務境界を作ろうという発想は、大規模なプロジェクトでは確かに有効な選択肢です。

問題は、Serviceレイヤーの導入が形骸化しやすい 点にあります。Serviceクラスに明確な命名規約や配置ルール、責務範囲のガイドラインがない場合、「とりあえずServiceに書いておくか」という空気が生まれ、結果としてControllerから呼び出されるFat Serviceができあがってしまいます。Controllerから肥大化したサービスクラスへ移動しただけ――つまり何も解決していないのに、レイヤーが一段増えてしまっただけという状態は、設計をむしろ複雑にし、開発コストを増やしていきます。

この落とし穴を避けるには、「何をServiceに書くか」ではなく「何をServiceに書かないか」を先に決めること が効きます。たとえば、次のような線引きをあらかじめ自分の中で決めておくと、Fat Service化を回避しやすくなります。

  • HTTPリクエストのバリデーションや認可判定は、FormRequestやPolicyに残す
  • 永続化の責務はEloquentやRepositoryに残し、Serviceは「複数の集約をまたぐロジック」に限定する
  • 命名は「アクション寄り(CreateOrderService)」「ドメイン寄り(OrderProcessor)」のいずれかに統一し、両方が混在しないようにする

そして何より、最初からServiceクラスを整え上げようとしないことです。まずはControllerからActionクラスに処理を移し、そこから再利用性が高まって「複数エンドポイントから呼ばれる共通処理」になってきたタイミングで、初めてServiceに昇格させる――このボトムアップの流れのほうが、過剰な抽象化を防ぎ、再現性の高い設計判断につながります。

観点Fat Model許容Actionクラス(ADR的)Serviceレイヤー導入
向いている規模小〜中規模、CRUD中心小〜中規模、画面/API単位で処理を分けたいケース中〜大規模、複数エンドポイントを横断する処理がある
学習コストの低さ◎(Eloquentの理解があれば始められる)◯(命名と__invokeだけ分かれば導入できる)△(レイヤーの責務設計に慣れる必要がある)
再利用性△(モデルが太ると一部だけ使い回せない)△(アクション単位なので再利用は限定的)◎(命名と責務が明快な場合に高い)
テストのしやすさ◯(Eloquentのファクトリが使える)◎(依存を最小化しやすい)◯(依存設計次第)
代表的な失敗パターンModel肥大化、ドメイン複雑化時の見通し悪化アクション数の増殖、ディレクトリの肥大化Fat Service化、責務の混在、過剰な抽象化

上表を見ていただくと、それぞれに得意・不得意があることが見えてきます。重要なのは「自分は今どの規模・どのドメイン複雑度にいるのか」を、落ち着いた目で観察すること です。

ADRパターンと1アクション1クラス ―現実的な解

ここ数年でLaravelコミュニティでも採用例が増えているのが、ADR(Action-Domain-Responder)パターンや、それに連なる「1アクション1クラス」のActionクラス設計です。具体的には、一つのユースケース(例:「注文を確定する」「ユーザー登録メールを送信する」)を一つのクラスとして定義し、__invokeメソッドだけを公開するというシンプルな構造を指します。これはクリーンアーキテクチャで語られるユースケース層に近い粒度を、Laravelの慣習的なディレクトリ構造の中でシンプルに再現するやり方とも言えます。

Laravelでこの設計と相性が良いのは、app/Actionsapp/UseCasesといった専用ディレクトリを切って、そこに責務を限定するやり方です。一つのクラスが肥大化する余地が構造的に生まれづらく、テストを書きやすいという副次的な利点もあります。クラス名を見ればその処理の意図が分かる――そんなディレクトリが育っていく安心感は、Controllerに何でも書いていた頃にはなかったものです。

Laravelの公式は、ServiceクラスやRepositoryパターンの標準ディレクトリ構造を意図的に定めていません。これは、プロジェクトごとに最適解が異なるというLaravelの設計思想の表れでもありますが、同時に開発者自身が「どの規模ならどの構造がフィットするか」を判断する力 を求められるということでもあります。

Actionクラスは、Fat Controller・Fat Serviceのいずれにも陥りにくく、しかも導入のハードルが低いため、個人開発でまず試してみる構成として優れています。Controllerが圧迫され始めたユースケースだけでもActionクラスに切り出してみると、驚くほどControllerがスリムになり、テストの記述量も減ったと感じられるはずです。まずは1アクション1クラスを徹底するくらい、軽い気持ちで始めてみましょう。Controller側に「下ごしらえだけ残し、主菜をActionに任せる」――そんな役割分担を、少しずつ育てていくのがおすすめです。

フェーズ別に見る設計判断 ―過剰な抽象化を避けるためのフレーム

最終的に、Laravelにおけるビジネスロジックの配置は、プロジェクトが今いるフェーズによって変わります。以下は、私がコードレビューの現場でよく使う判断基準のフレームです。あくまで目安ですが、「どの規模で何を採用すべきか」で迷ったときの地図として使っていただけると思います。

個人開発・MVP段階

Eloquent中心のスキニーController+多少のFat Model許容で十分です。Actionクラスも無理に導入する必要はなく、Controllerを圧迫し始めた処理だけが目に留まった段階で、そこだけ切り出す形にしましょう。ビジネスロジックの「置き場所」に悩んだら、その時点で初めて選択肢を考える――くらいのリラックスした距離感が、再現性を損ないません。個人開発は「動かし続ける」ことが最優先で、抽象化の完成度ではないということは、意識しておくと判断がブレにくくなります。

小〜中規模・複数人で触れ始めたフェーズ

Actionクラスをメインに据え、再利用性の高い処理だけServiceに昇格させる方針が見通しと保守性のバランスを取りやすいです。「Controllerは薄く」「Actionクラスは単機能」「Serviceは共通処理置き場」――この三層だけで、かなりのプロジェクトは綺麗に保つことができます。チームに新しいメンバーが加わったとき、彼女・彼がまず読むのはControllerとActionのはずです。そこに「今のプロジェクトの心臓部」が集約されている状態を作っておけると、教育コストも下がっていきます。

中〜大規模・ドメインが複雑化したフェーズ

Serviceレイヤー、Repositoryレイヤー、場合によってはValue ObjectやDomain Serviceを部分的に導入し、Eloquentは永続化の役割に専念させる構成が安定します。ADRパターンの徹底も検討して良いタイミングです。ただし、ここでも全レイヤーを一気に立ち上げるのではなく、複雑になっている箇所から段階的に導入する のが鉄則です。テストのカバレッジが低い領域や、頻繁に仕様変更が入っている箇所から手をつけていく――その順番を守るだけで、移行プロセス全体がぐっと再現性のあるものになります。

ここで注意していただきたいのは、最初から完璧なレイヤリングを目指さない ということです。過剰な抽象化は、個人開発では特に、コストに見合わない「後から剥がせない足枷」になりやすいです。まずはEloquentの素直さで書き、ドメインが育ってきた段階でActionクラスやServiceクラスを「必要な部分だけ」導入していく――その再現性のあるプロセスが、結果的に一番確実な選択になります。

「正解の設計をインストールする」のではなく、「今のプロジェクトにフィットする設計を選ぶ目」を育てていく。それが、Laravelでの長期的な開発を最も楽にします。

よくある質問

Laravelでビジネスロジックをどこに書くべきですか?
プロジェクトの規模やフェーズによります。小規模ならEloquentモデルを活用し、複雑化してきたらActionクラスやServiceレイヤーへの切り出しを検討するのが一般的です。
Fat Modelは避けるべきですか?
必ずしも避けるべきではありません。小〜中規模のプロジェクトであれば、Eloquentの機能を活かしてモデルにロジックを集約させることで、開発効率を高めることができます。
Serviceレイヤーを導入する際の注意点はありますか?
明確なルールがないと「Fat Service」化して設計が複雑になる恐れがあります。まずは何をServiceに書かないかを決め、再利用性が高い処理のみを昇格させるのが賢明です。
Actionクラスとはどのような設計ですか?
一つのユースケースを一つのクラスとして定義し、__invokeメソッドのみを公開する設計です。Controllerをスリムに保ちやすく、テストも書きやすいという利点があります。
個人開発ではどのような構成が適していますか?
Eloquent中心のスキニーControllerと、多少のFat Modelを許容する構成が適しています。過剰な抽象化を避け、必要に応じて部分的にActionクラスを導入する程度が効率的です。

参考情報