導入
はじめまして、僕は新卒エンジニアです。
本記事は、Laravel初学者が必ず通る、Fat Controllerについて話していきます。
僕もかなり長い間、これを続けていました。
昔のリポジトリなんかを見返すと、目を背けたくなります。
なぜこれに陥りがちかというと、脱するには視座を上げる必要があるためです。
Laravel強いてはMVCパターンの解釈を正しく、明確に、細分化しなければなりません。
より抽象度の高い話になりそうですが、なるべく具体も交えていきます。
- CRUDは書けるけど、設計に悩む1-2年目
- 動きはするけど、より良いコードを書けるようになりたい
こんなことを考えている方に読んでほしいです!
Fat Controllerについて
そもそも「Fat Controller」とはなんぞや、という方に向けて、簡単に説明していきます。
一言で、「コントローラが肥大化している」ということです。
以下は例です。
class SearchController extends Controller
{
public function searchScope(Request $request){
$areas = Area::all();
$genres = Genre::all();
$favorites = Favorite::where('user_id',Auth::id())->get('salon_id');
$salons = Salon::AreaSearch($request->salon_area)->GenreSearch($request->salon_genre)
->KeywordSearch($request->salon_name)->get();
$counts = $array->count();
$salonIdArray = [];
for($start=0;$start<$counts;$start++){
array_push($salonIdArray,$array[$start]["salon_id"]);
}
return view('salons',compact('salons','areas','genres','salonIdArray'));
}
}
以下は、想定です。
- サロン(美容室など)の口コミサイトを開発中(個人でできる簡易的なもの)
- サロンの名前を検索欄に入力し、検索結果に該当するサロンが表示される
- 検索に関する処理を行うSearchControllerのsearchScopeメソッドについて取り上げる
このメソッドで行いたいことは、
「ジャンルや地域、店舗名によって検索された場合、検索結果を表示したい」
です。
このメソッド内が "肥大化" しています。
このメソッドが今現在行っていることは、以下です。
- Area、Genreクラスからall()メソッドを呼び出し、変数に格納
- ログインユーザのお気に入り店舗を取得、変数に格納
- エリア、ジャンル、キーワードなどをもとに検索処理、変数に格納
- ログインユーザのお気に入り店舗のIDを配列に格納
- 変数と共に、viewを返す
"肥大化" というのは、コントローラが行うべき処理の範疇をはみ出た処理までもが定義されていることを意味しています。
どのような部分が逸脱しているのか、または好ましいのか、見ていきましょう。
果たすべき責務
この「責務」という考え方を理解できると、プログラミング学習において、1つ視座が上がるでしょう。
Laravelというフレームワークというよりも、オブジェクト指向という1つの概念について勉強されると、理解しやすいと思います。
簡単に説明すると、「1つのクラスは必ず1つの責務を果たすべき」 という考え方です。
ではコントローラにはどんな責務があるのでしょうか。
Laravelが採用しているデザインパターンは
MVC(Model - View - Controller)です。
それぞれに果たすべき責務があります。
以下の記事にとても良いまとめ方がされていたため、引用させていただきます。
- モデル
取り扱うデータの構造と、それに紐づくビジネスロジックを管理する
- ビュー
HTTPリクエストにおける、エンドポイントの見え方の構造を定義する
- コントローラ
ビューとモデルの媒介を行い、必要に応じて横断的なアプリケーションのビジネスロジックの呼び出しを定義する
ものすごく良い書き方をされているなと、感銘を受けました。
コントローラには、以下のように書かれています。
「横断的なアプリケーションのビジネスロジックの "呼び出し" を定義する」
あくまで、コントローラで定義するべきは呼び出しなのです。
処理そのものをコントローラで行うべきではありません。
それらはモデルなどの他クラスに委譲すべきなのです。
先ほどの言い回し風に、
「呼び出しを定義することは責務範囲内であるが、処理そのものを定義することは責務範囲外である」
ということです。
:処理そのものの定義を他に委譲することのメリット
- メンテナンスしやすくなる
- もし修正した処理ができた時、「〇〇メソッド内の一部分」を探すよりも、「機能クラス」を探す方が簡単ですよね
- テストが容易に
- 処理を外部に委譲しているため、独立していると言えます。「単体テスト」がしやすくなります
- 拡張性が上がる
- 何か新しく機能を追加したくなったとき、既存機能との折り合いをつけやすくなります
責務を全うしたコントローラ
好ましい書き方をすると、以下の感じに
class SearchController extends Controller
{
public function SearchScope(Request $request){
$areas = Area::all();
$genres = Genre::all();
$favorites = Favorite::findById(Auth::id());
$salons = Salon::generalSearch();
$favorites_array = FavSalonService::createFavoritesIdsArray($favorites);
return view('salons',compact('salons','areas','genres','favorites_array'));
}
}
不細工なコードは、少しスマートになりました。
※IDの配列を返すだけなのであれば、以下の書き方をすれば、もう1行減らせます。
$favorites = Favorite::findById(Auth::id())->pluck('id');
削除:$favorites_array = FavSalonService::createFavoritesIdsArray($favorites);
pluckメソッドを用いると、指定したプロパティのみの配列を返すことができます。
記事をより良いものにするために、筆者の過去の過ちをそのまま載せています(笑)
以下3つの処理を他クラスに委譲しました
- ログインユーザのお気に入り店舗を取得
- エリア、ジャンル、キーワードなどをもとに検索処理
- ログインユーザのお気に入り店舗のIDを配列に格納
他クラスで定義し、それを呼び出すだけの構造にしています。
1と2はモデルに委譲し、3はサービスクラスに委譲しています。
※3だけ異なる理由は、以下のコラムで
コラム
気になる人だけ読んでください!!
3のみモデルではない理由は、複数のモデルをまたがるためです。
「ログインユーザがお気に入り登録している店舗のIDを配列として返す」ということを実装しています。
ということは、「店舗(Salon)」と「お気に入り(Favorite)」という2つのモデルをまたがっています。
そのため、どちらかのモデルに定義してもそれは不自然となるのです。
こういった時のために、サービス層というレイヤーが存在します。
気になる人は、「DDD」「ドメインサービス」「アプリケーションサービス」などのワードについて調べてみてください。
多分少し難しいです。
※プロダクト自体が比較的大きくない、デプロイまでのスピード感を重要視しているなどの場合、処理を複数のレイヤーに分ける、別のクラスに委譲するメリットはそこまでありません。
結論
Laravelというフレームワークでwebアプリケーションを開発しようと努力することは素晴らしいことです。
徐々にLaravelを理解していき、アプリケーションが動いた時の達成感も大きいです。
ですが、フレームワークという利便性に溺れ続ける怠慢だけは、許してはいけません。
筆者は、構造や本質を理解しようともせず、恩恵を受け続けることこそ怠慢だと考えています。
フレームワークは、すでに用意された恩恵を受けることができ、独特の仕様に精通さえすれば、アプリケーションを開発することができる素晴らしい骨組みです。
ですが、その恩恵を受けるがあまり、本質的な理解は全く進まず、あるポイントで停滞することが考えられます。
ある程度Laravelを理解できたな、という方こそ、オブジェクト指向を勉強されてはいかがでしょうか。
フレームワークの見方も変わってきますし、どんな言語、フレームワークにも通ずる基礎力が身につくでしょう。
ここまで読んでくださった方々、ありがとうございました!
いいねを押してくれると、僕の自己肯定感が上がります!
ご質問またはご指摘がある方は気軽にコメントしてください!喜んで回答します!