こんにちは。Enjoy IT Life管理人の@nishina555です。
Railsアプリケーションが成長するにつれて、モデルにフォーム固有のバリデーションやロジックが増えてFat Modelになりがちです。
この記事では、フォームとモデルを分離するためのForm Objectパターンについて、どんな場面で使うべきか、具体的な実装方法はどうなるのかをまとめます。
Form Objectとは
Form Objectは、Railsのクラス分割パターンのひとつで、フォームの入力データを管理する専用のクラスです。form_withやform_forに渡すオブジェクトとして利用されます。
モデルに直接バリデーションを追加する代わりに、Formクラスにバリデーションを定義することで、モデルをシンプルに保てます。
Form Objectが必要になる場面
以下のような場面では、Form Objectの導入を検討する価値があります。
fields_forやaccepts_nested_attributes_forを使って、1つのフォームで複数のモデルに対して同時に処理を行う場合 —accepts_nested_attributes_forの代替案としてForm Objectが有効です- 同一のモデルに対して複数の画面で新規作成・更新処理を行い、状況によってバリデーションが異なる場合 —
validatesのifやonオプションを使うケースで、モデルの条件付きバリデーションが複雑になっている場合に有効です - 1つのアクションでフォームに表示したモデルの作成・更新処理以外の処理を行う場合 — モデルの
after_create等で別の処理を実行しているケースが該当します
Form Objectのメリット
Form Objectを導入するメリットは以下の通りです。
フォームごとのバリデーションをモデルに依存せずに定義できることが最大のメリットです。Form Objectがない場合、各フォームで有効になるバリデーションをモデル側で設定する必要があり、条件付きバリデーション(このフォームのときはこのバリデーションが有効、など)が増えて複雑さが増してしまいます。
また、モデルを汚すことなくform_withやform_forを利用できる点もメリットです。
Form Objectの注意点
保存や検索処理のビジネスロジックをForm Objectに書いていくと、かなり見通しの悪いコードになり、Form Objectの役割が一気に増えてしまいます。
ビジネスロジックが増えてきたら、サービスクラスを新たに作成してロジックをForm Objectからサービスクラスへ移動させましょう。Form Objectではサービスクラスへ引数を渡してメソッドを実行する形にします。
実装例1: 複数モデルを扱うサインアップフォーム
ユーザーと会社を同時に作成するサインアップフォームの例です。
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :company
attribute :name, String
attribute :company_name, String
attribute :email, String
validates :email, presence: true
# ...その他のバリデーション...
# フォームそのものは決して永続化しない
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
@company = Company.create!(name: company_name)
@user = @company.users.create!(name: name, email: email)
end
end
class SignupsController < ApplicationController
def create
@signup = Signup.new(params[:signup])
if @signup.save
redirect_to dashboard_path
else
render "new"
end
end
end
コントローラーはモデルを直接操作する場合と同じインターフェースで利用できるため、既存のコードとの一貫性を保てます。
実装例2: ActiveModel::Modelを使ったフィードバックフォーム
データベースへの保存が不要なフォーム(例: お問い合わせフォーム)の例です。ActiveModel::Modelをincludeすることで、バリデーションやform_withとの連携が可能になります。
class Feedback
include ActiveModel::Model
attr_accessor :title, :body
validates :title, :body, presence: true
def save
return false if invalid?
AdminMailer.feedback(title, body).deliver_later
true
end
end
class FeedbacksController < ApplicationController
def new
@feedback = Feedback.new
end
def create
@feedback = Feedback.new(feedback_params)
if @feedback.save
redirect_to home_path, notice: 'フィードバックを送信しました'
else
render :new
end
end
private
def feedback_params
params.require(:feedback).permit(:title, :body)
end
end
ビューでは通常のモデルと同じようにform_withが使えます。
<%= form_with model: @feedback, local: true do |f| %>
<% if @feedback.errors.any? %>
<% @feedback.errors.full_messages.each do |message| %>
<%= message %>
<% end %>
<% end %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :body %>
<%= f.text_area :body %>
<%= f.submit %>
<% end %>
まとめ
- Form Objectは、フォームとモデルを分離するためのクラス分割パターン
- 複数モデルへの同時操作や、画面ごとに異なるバリデーションが必要な場面で有効
- ビジネスロジックが増えてきたらサービスクラスに切り出す
ActiveModel::Modelをincludeすることでform_withとの連携が容易になる