メインコンテンツまでスキップ

第2部の導入 — Fat Controller/Fat Modelとは何か

第1部では、フレームワークに依存しないPHPのクラス設計原則を扱いました。 この章から始まる第2部では、その原則をLaravelの実務コードに応用していきます。

Laravelの基礎に不安がある場合

第2部は、Laravelの基本的なMVC構造・ルーティング・Eloquentの基本的なCRUD操作を経験済みであることを前提とします。 不安がある場合は、先にLaravel × DDD × クリーンアーキテクチャ実践入門はじめにLaravel API開発の基礎を読むことをおすすめします。

Fat Controllerとは

次のOrderControllerを見てください。

❌ Bad: Fat Controller
class OrderController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'items' => 'required|array',
'customer_email' => 'required|email',
]);

$total = 0;
foreach ($validated['items'] as $item) {
$total += $item['price'] * $item['quantity'];
}

foreach ($validated['items'] as $item) {
$stock = DB::table('stocks')->where('name', $item['name'])->value('quantity');
if ($stock < $item['quantity']) {
return response()->json(['error' => '在庫が不足しています'], 422);
}
}

$order = Order::create([
'customer_email' => $validated['customer_email'],
'total' => $total,
]);

Mail::raw("ご注文ありがとうございます。合計{$total}円です。", function ($message) use ($validated) {
$message->to($validated['customer_email'])->subject('注文確認');
});

return response()->json($order, 201);
}
}

バリデーション・合計金額の計算・在庫確認・注文の作成・確認メールの送信が、storeメソッド1つに詰め込まれています。 このように、業務ロジックがControllerに溜め込まれ肥大化した状態を、Fat Controllerと呼びます1。 MVCで最もよく見られるアンチパターンの1つです。

Fat Modelとは

「Controllerを痩せさせよう」と考えて、処理をそのままモデルに移すとどうなるでしょうか。

❌ Bad: 今度はFat Modelになる
class Order extends Model
{
public static function placeOrder(array $items, string $customerEmail): self
{
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}

foreach ($items as $item) {
$stock = DB::table('stocks')->where('name', $item['name'])->value('quantity');
if ($stock < $item['quantity']) {
throw new RuntimeException('在庫が不足しています');
}
}

$order = self::create([
'customer_email' => $customerEmail,
'total' => $total,
]);

Mail::raw("ご注文ありがとうございます。合計{$total}円です。", function ($message) use ($customerEmail) {
$message->to($customerEmail)->subject('注文確認');
});

return $order;
}
}

処理の置き場所がControllerからModelに移っただけで、1つのメソッドに複数の責務が詰め込まれている状態は変わっていません。 これは、第1章で見たgod classと同じ構造の問題です。

Laravelの作者であるTaylor Otwellは、Controllerを薄く保ちModelを厚くする方針を好んで語っています2。 ただし、ここでの「厚い」は、行数が多いという意味ではなく、他のServiceやコマンドを呼び出す公開APIが豊富だという意味です。 バリデーション・在庫確認・通知という異なる関心事を1つのメソッドに詰め込んだ、上のOrderクラスとは区別してください。

第2部で扱う範囲

第2部では、第9章でEloquentモデルの責務、第10章でサービスクラスへの責務分割を扱い、Fat Controller・Fat Modelのどちらも避ける設計を見ていきます。

一方で、次の内容には踏み込みません。

  • DDD戦術パターン(値オブジェクト・エンティティ・集約・リポジトリパターンなど)
  • クリーンアーキテクチャの層実装(ユースケース層・プレゼンテーション層・トランザクション境界の管理など)

これらは、Laravel × DDD × クリーンアーキテクチャ実践入門が扱う領域です。 第10章のサービスクラス抽出も、大きくなったクラスを分割するというクラス設計の視点にとどまり、トランザクション境界の管理やドメインイベントの発行といった、ユースケース層としての設計には立ち入りません。

まとめ

  • Controllerに業務ロジックが詰め込まれ肥大化した状態をFat Controllerと呼ぶ
  • 処理をModelに移しただけでは、詰め込みの構造自体は変わらずFat Modelになりうる
  • 第2部は、ここまでの原則をLaravelに応用してこの2つを避ける設計を扱う
  • DDD戦術パターンやクリーンアーキテクチャの層実装は、既存連載が担う範囲として立ち入らない

次に読む

次章では、Eloquentモデルの責務を扱います。 モデルに書いていい責務と、書くべきでない責務の判断基準を見ていきます。

練習問題

次の処理を書く場所として、Controller・Model・Serviceクラスのどれが適切か、理由とともに答えてください
  1. リクエストのバリデーション
  2. 在庫確認と注文作成をまとめた一連の処理
  3. データベースへの保存(Eloquentのクエリ)

解答例

  1. Controller(またはForm Request): リクエストの形式チェックは、Controllerの手前で行う入り口の処理です
  2. Serviceクラス: 複数の関心事にまたがる一連の処理は、第10章で扱うサービスクラスの責務です
  3. Model: 個々のデータの永続化は、Eloquentモデル自身の責務です

この判断基準は、第9章・第10章で詳しく扱います。


Footnotes

  1. 出典: Put your Laravel controllers on a diet。Fat ControllerはMVCで最も一般的なアンチパターンの1つとして知られています。

  2. 出典: Taylor Otwell: "Thin" Controllers, "Fat" Models Approach