はじめに
Railsで開発をしていると、機能追加のたびにControllerやModelへ処理を書き足していき、気づけば1ファイルがかなりの行数になっていたという経験はありませんか。
いわゆる「ファットコントローラー」「ファットモデル」と呼ばれる状態です。
こうした状態への対策として「Skinny Controller, Fat Model」という考え方がよく紹介されます。ただ、この言葉をそのまま受け取って「Controllerを薄くするためにModelへ全部書けばいい」としてしまうと、今度はModelが肥大化するだけになってしまいます。
実際には、ControllerやModelだけで処理を抱え込まず、責務に応じて適切な場所へロジックを分割することが大切です。
この記事では、ファットモデル・ファットコントローラーを避けるための代表的な設計パターンの一つであるService Objectについて整理してみます。
Service Objectとは
Service Objectは、特定のユースケースに関する処理を専用のクラスへ切り出すための設計パターンです。
例えば、
- 注文を作成する
- 決済を行う
- 会員登録とメール送信をまとめて行う
のように、複数のモデルや外部サービスをまたぐ処理を書くときによく利用されます。
Controllerに書くには複雑で、複数のModelや外部サービスにまたがる処理を切り出したい場合に向いています。
一般的には app/services 配下にクラスを作成し、call メソッドを入り口として実装することが多いです。
実装例
# app/services/create_order_service.rb
class CreateOrderService
def initialize(user, params)
@user = user
@params = params
end
def call
order = @user.orders.create!(@params)
# 外部決済サービスとの連携など
PaymentService.new(order).call
OrderMailer.completed(order).deliver_later
order
end
end
Controller側はシンプルになります。
def create
@order = CreateOrderService.new(current_user, order_params).call
redirect_to @order
end
※ 実際には外部サービスとの連携では、失敗時のデータ整合性や例外処理、リトライ処理なども考慮する必要がありますが、この記事ではService Objectの役割を分かりやすくするため省略しています。
Service Objectは特定のフレームワーク機能に依存せず、通常のRubyクラスとして実装できるため、比較的導入しやすい設計パターンです。
その他の設計パターン
Railsでは、ファットモデルやファットコントローラーを避けるために、Service Object以外にもさまざまな設計パターンがあります。
| パターン | 主な用途 |
|---|---|
| Service Object | ユースケース単位の業務処理をまとめたいとき |
| Concern | 複数のModelやControllerで共通する機能や処理を切り出したいとき |
| Form Object | フォーム入力や複数モデルのバリデーション・保存をまとめたいとき |
| Query Object | 検索条件やJOINが複雑になり、scopeだけでは見通しが悪くなったとき |
| Policy Object | 認可ロジックを分離したいとき |
| Decorator / Presenter | View表示用のロジックをModelから切り離したいとき |
| PORO(Plain Old Ruby Object) | ActiveRecordを継承しない通常のRubyクラスとして、ドメインロジックを切り出したいとき |
今回はService Objectに絞って紹介しましたが、他の設計パターンについても、機会があれば整理して記事にしてみたいと思います。