Rails 7〜8 時代のアプリ設計では、モデルやコントローラの肥大化を防ぐための中心的な概念として PORO が強く推奨されます。
PORO(Plain Old Ruby Object)とは、ActiveRecord などのフレームワーク機能に依存しない、ただの Ruby クラス のことです。
サービスクラス・フォームオブジェクト・クエリオブジェクトなど、Rails でビジネスロジックを整理するための代表的な手法がここに含まれます。
Rails 3〜4 の頃はモデルに何でも詰め込む傾向でしたが、 Rails 7〜8 では モデルはデータの責務に限定 することが推奨されています。
PORO は Rails に依存しないため:
といったメリットがあります。
コントローラは「値の受け渡し」だけに集中し、 実処理は PORO に寄せる のが標準的な構成です。
複雑なビジネスロジックを 1 クラスにまとめる方式。 Rails 8 時代でも最もよく使われる PORO。
▼ PORO側
# app/services/post/create_service.rb
class Post::CreateService
def initialize(user:, params:)
@user = user
@params = params
end
def call
Post.create(@params.merge(user: @user))
end
end
▼ コントローラ側
def create
result = Post::CreateService.new(
user: current_user,
params: post_params
).call
redirect_to result
end
複数モデルをまたぐフォームや、独自バリデーションをまとめたいときに利用します。
Rails 7〜8 では ActiveModel::Model を使う方式が定番。
▼ 例
# app/forms/user_signup_form.rb
class UserSignupForm
include ActiveModel::Model
attr_accessor :email, :password
validates :email, presence: true
validates :password, length: { minimum: 8 }
def save
return false unless valid?
User.create(email: email, password: password)
end
end
複雑な SQL や検索処理をモデルから切り離すパターン。
# app/queries/post_search_query.rb
class PostSearchQuery
def initialize(keyword:)
@keyword = keyword
end
def call
Post.where("title LIKE ?", "%#{@keyword}%")
end
end
ビューでの表示ロジックをまとめる方式。 Rails 8 でも delegate を使った簡易 Presenter が一般的。
class PostPresenter
def initialize(post)
@post = post
end
def formatted_title
"★ #{@post.title}"
end
end
非同期 UI が主流となり、 コントローラは役割を限定する必要性が増した。
複雑な処理をコールバックに書くと、 Turbo との組み合わせでバグになりやすい。
→ その処理は PORO に逃がすのがベスト。
PORO は Rails に依存しないため System Spec や Request Spec を書かずとも 純粋な Ruby 単体テストで検証できる。
require "rails_helper"
RSpec.describe Post::CreateService do
it "投稿を作成できる" do
user = create(:user)
service = described_class.new(user: user, params: { title: "abc" })
post = service.call
expect(post.title).to eq("abc")
expect(post.user).to eq(user)
end
end
Rails 8 時代の PORO は次の役割を担います。