※本記事は「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がまだ現役」「イベント駆動が標準装備」という点が特に新鮮だった。
コメント