RailsとLaravelで違う責務分離の考え方|Service・Observer・UseCase比較

RailsからLaravelを眺める

※本記事は「RailsからLaravelを眺める」シリーズの第3回です。Rails出身の私がLaravelを触りながら、Railsと比較して違いを整理していく連載になります。


はじめに

この第3回では、Observer / Service 層 / UseCase / イベント駆動について掘り下げます。
RailsとLaravelで「責務分離」をどう考えるかは文化的にも思想的にも大きく違います。Rails出身者として触れていて「ここは意外だ」と感じた部分を整理していきます。


Railsにおける世界観

Railsは「Fat Model, Skinny Controller」が基本思想。その中で責務分離をどう行うかは、開発チームやプロジェクトの文化に強く依存します。

責務分離のベストプラクティス(Rails)

Rails公式が提供しているのは基本的にMVC構造のみですが、実務ではこれだけではすぐにモデルが肥大化します。現実的なベストプラクティスを整理すると次のようになります。

  • Controller
    I/Oの境界。HTTPリクエストとレスポンスの橋渡しに専念し、ロジックは持たない。
  • Model (ActiveRecord)
    永続化とドメインの振る舞いを担う。ただしコールバックに副作用を積みすぎるとスパゲッティ化するため、極力シンプルに保つ。
  • Form Object (ActiveModel)
    入力検証が複雑な場合はPOROにActiveModelをミックスインしてフォームオブジェクト化。これによりモデルを痩せさせつつ、バリデーションのUXを維持できる。
  • Mailer / Job
    メール送信や重たい処理は Action Mailer / Active Job へ委譲。非同期処理が標準で用意されているため、レスポンスを塞がない設計にするのが基本。
  • Concern
    複数のクラスで共有したい“薄いロジック”だけを抽出。便利だが、巨大な処理を詰め込むと責務が見えづらくなるため乱用は禁物。
  • ActiveSupport::Notifications
    「起きたこと」を publish し、別の箇所で subscribe できる仕組み。ログ収集やメトリクスなど、疎結合な付帯処理に有効。

Serviceクラスの位置づけと賛否

複数モデルを跨ぐ長い取引や外部APIとの連携など、調停が必要な処理はServiceクラスに切り出すのが現実解です。
ただしこのService導入には常に賛否があります。

  • 賛成派
    • モデルを痩せさせて読みやすくできる
    • テスト容易性や再利用性が上がる
    • トランザクションの境界を見通し良くまとめられる
  • 反対派
    • サービスに「何でもかんでも」積み込んでしまい、結果的に責務が散らかる
    • Rails Way(ActiveRecord中心)の一貫性が失われる

つまりRailsのServiceは“オーケストレーション専用”に徹するなら有効、万能箱にすると破綻する、という扱いが実務的な落としどころです。

Observer(Rails)

かつて ActiveRecord::Observer が存在しましたが、Rails 4で削除され、現在はgemに分離されています。

class UserObserver < ActiveRecord::Observer
  def after_create(user)
    WelcomeMailer.send(user).deliver_later
  end
end

Observerは「責務が見えにくい」「暗黙的すぎる」としてレガシー扱いになり、現在はコールバックやServiceクラスに寄せるのが一般的です。


Laravelにおける世界観

Service層の温度感

Laravelには明確な「Service Object文化」は存在しませんが、app/Services を切って責務を分ける実装は普通に行われています。
Railsと異なるのは、DIコンテナが公式で強力に組み込まれているため「必要に応じて切る」程度で十分であり、Railsのように強い賛否論争には発展しにくいことです。

Observer(Laravel)

LaravelではObserverが公式に現役の仕組みです。

// app/Observers/UserObserver.php

class UserObserver
{
    public function created(User $user): void
    {
        Mail::to($user->email)->send(new WelcomeMail($user));
    }
}

Eloquentモデルのライフサイクルイベント(creating, created, saving, deleting…)にシンプルにフックでき、
「Observerが消えたRails」とは対照的に「Observerを積極的に使っていい」という立場です。

UseCase(アプリケーション層)

Rails出身者としてしっくり来るのは、LaravelでもビジネスロジックをUseCaseクラスに集約する設計です。
Observerはキャッシュ削除やログといった軽量な副作用専用にして、本質的な処理はUseCaseに閉じ込めるのが筋が良い。


Laravel実装例

UseCase

final class CreateOrderUseCase {
  public function __construct(private OrderRepository $orders) {}
  public function __invoke(CreateOrderInput $in): Order {
    return DB::transaction(function () use ($in) {
      $order = $this->orders->createFor($in->userId, $in->items);

      // 出来事だけを発火。副作用はリスナーに任せる
      event(new OrderCreated($order->id));

      return $order;
    });
  }
}

イベント / リスナー

class OrderCreated {
  use \Illuminate\Foundation\Events\Dispatchable;
  public function __construct(public int $orderId) {}
}

class SendOrderConfirmation implements \Illuminate\Contracts\Queue\ShouldQueue {
  public function handle(OrderCreated $event): void {
    $order = \App\Models\Order::find($event->orderId);
    \Mail::to($order->user)->send(new \App\Mail\OrderConfirmation($order));
  }
}

Observer

class OrderObserver {
  public function deleted(Order $order): void {
    \Cache::forget("order:{$order->id}");
  }
}

Observerとイベント/リスナーの使い分け

  • Observer … モデルライフサイクルに直結する軽量な副作用(キャッシュ削除・監査ログなど)
  • イベント/リスナー … UseCaseから「出来事」を発火し、重い処理や外部連携を後段に逃がす。非同期処理とも相性が良い

Rails出身者から見た違い

  • Rails
    • Observerはレガシー扱い
    • 調停が必要な処理はServiceクラスに切り出す(ただし賛否あり)
    • MVC以外の補助はFormObjectやJobなどを駆使して整理
  • Laravel
    • Observerは現役で公式サポート
    • ビジネスロジックはUseCaseに集約しやすい
    • イベント/リスナーが標準装備され、副作用の外出しが自然

まとめ

  • RailsはMVCを基本に、FormObject・Job・Mailer・Concern・Notificationsを駆使しつつ、調停処理はServiceクラスに置くのが現実解。ただし導入には賛否がある。
  • LaravelはObserverが公式現役で、さらにイベント/リスナーが標準装備。ビジネスロジックはUseCase、副作用はObserverやイベントに外出しが自然。
  • Rails出身者にとっては「Observerがまだ現役」「イベント駆動が標準装備」という点が特に新鮮だった。

コメント

タイトルとURLをコピーしました