Day 6・責務の分離とデータ整合性 — サービス層・トランザクション・排他制御
サービス層 — ビジネスロジックの置き場所
業務手順を専用クラスに集約し、コントローラ・モデルと責務を分担。DIで受け取る。
サービス層とは
とは、アプリの『業務手順(ビジネスロジック)』を担当する専用クラスの層です。前レッスンの注文処理のように、複数のモデルをまたいで『在庫を減らし、注文を作り、ポイントを付け、メールを送る』といった一連の手順を、ここに一つのメソッドとしてまとめます。
三者の責務分担
- コントローラ: HTTPの入出力。リクエストを受け、サービスを呼び、結果をレスポンスにする。
- サービス層: 業務手順の調整。複数モデルをまたぐ処理の順序や条件を決める。
- モデル: 1つのテーブルに対応するデータと、そのデータ自身に閉じたルール。
悪い例と良い例
コントローラに全部書く
public function store(Request $request)
{
// 在庫チェック・金額計算・在庫更新・注文作成・
// ポイント付与・メール送信…すべてここに直書き
$product = Product::find($request->product_id);
if ($product->stock < $request->quantity) { /* ... */ }
$amount = /* 割引計算 */;
$product->decrement('stock', $request->quantity);
$order = Order::create([/* ... */]);
$request->user()->increment('points', /* ... */);
Mail::to($request->user())->send(/* ... */);
return redirect()->route('orders.show', $order);
}サービスに委譲する
public function store(
StoreOrderRequest $request,
OrderService $orders
) {
// 業務手順は OrderService に任せ、
// コントローラは呼び出しと結果の受け渡しだけ
$order = $orders->place(
$request->user(),
$request->validated()
);
return redirect()->route('orders.show', $order);
}サービスクラスの中身
class OrderService
{
public function place(User $user, array $data): Order
{
$product = Product::findOrFail($data['product_id']);
if ($product->stock < $data['quantity']) {
throw new OutOfStockException();
}
$amount = $this->calculateAmount($user, $product, $data['quantity']);
$product->decrement('stock', $data['quantity']);
$order = Order::create([
'user_id' => $user->id,
'product_id' => $product->id,
'quantity' => $data['quantity'],
'amount' => $amount,
]);
$user->increment('points', (int) ($amount * 0.01));
Mail::to($user)->send(new OrderCompleted($order));
return $order;
}
// 金額・割引の計算はプライベートメソッドに分けて見通しをよくする
private function calculateAmount(User $user, Product $product, int $qty): int
{
$amount = $product->price * $qty;
return $user->isPremium() ? (int) ($amount * 0.9) : $amount;
}
}private ロジックを単体テストしたくなったら
上の calculateAmount は private なので、place() 越しにしかテストできません。割引計算そのものをピンポイントで単体テストしたい場合は、計算を public なメソッドや専用クラス(例: AmountCalculator)へ切り出すのが定石です。テストしやすさは『どこに何を置くか』という設計の指針にもなります(Day7のテストのレッスンで、切り出した計算を単体テストする例を見ます)。
なぜ業務手順をサービス層に集約するのか
複数モデルをまたぐ手順は、どのモデルにも属しません。コントローラに書けば再利用できず、無理に片方のモデルに押し込めば別のモデルへの依存が漏れます。中立なサービス層に置くことで、コントローラ・バッチ・APIのどこからでも同じ手順を呼べ、割引計算のような部分だけを単体テストできます。責務が『どこに何があるか』の形で整理されるのが利点です。なお、規模が大きくなり、データの取得・保存の詳細までサービスから切り離したくなった場合は、データアクセスを専用クラスに集約する (リポジトリパターン)を併用する選択肢もあります。
DI(依存性の注入)で受け取る
上の例では、コントローラのメソッド引数に「OrderService」を型で書くだけで、Laravelが自動的にインスタンスを作って渡してくれます。これが (依存性の注入)です。自分で「new OrderService()」せずに外から受け取ることで、テスト時には偽物(モック)に差し替えられ、依存関係が疎になります。
// アンチパターン: クラス内部で依存を固定してしまう
class OrderService
{
public function place(...)
{
$mailer = new SmtpMailer(); // テストで差し替え不可
}
}
// 良い例: コンストラクタで受け取る(DI)
class OrderService
{
public function __construct(private Mailer $mailer) {}
// $this->mailer はテスト時にモックへ差し替えられる
}サービスコンテナが解決する
型ヒントを見て必要な依存を組み立てて渡す仕組みは (サービスコンテナ)が担います。開発者は『何が必要か』を型で宣言するだけで、『どう作るか』はフレームワークに任せられます。
面接ではこう言う
「複数モデルをまたぐ業務手順はサービス層に集約します。コントローラはHTTP、モデルは自テーブルのデータ、サービスは業務手順、と責務を分けます。サービスはコンストラクタインジェクションで依存を受け取るので、テストではモックに差し替えられますし、バッチやAPIからも同じ手順を再利用できます」
読んでも腑に落ちない所は、AIに噛み砕いてもらいましょう。
理解度チェック
答えられれば、面接で説明できる状態に一歩近づきます。
サービス層に置くのに最も適した処理はどれですか。
DI(依存性の注入)でサービスを受け取る利点として最も適切なものは?
コントローラ・サービス層・モデルの責務分担を説明してください。
記述式です。まず自分の言葉で答えを考えてから、模範解答を開いて自己採点してください。