※ こちらは別アカウントで2025年11月4日に公開したものをアカウント統合のため移植したものです。
はじめに
Kaigi on Rails2025に参加しました。Ruby Kaigiには過去3回参加しましたが、Kaigi on Railsの参加は初めてだったかな...?たしか去年はデスマーチだった記憶...(遠い目)
少し前にFormObjectという便利そうなものがあると知って気になっていたのですが、今回のセッションでもテーマとして取り上げられていたので、これを機に調べてみました。
FormObjectとは
FormObjectに関するいくつかのコンテンツで参照されていたこちらの記事によると、
FormObjectとは、
form_withのmodelオプション*1にActive Record以外のオブジェクトを渡すデザインパターンです。
*1:もしくはform_for
私はてっきりFormObjectとはActiveRecordの機能のひとつだと思っていたのですが、設計パターンなんですね。
どおりで Railsガイド でFormObjectとは出てこないわけだ...
また、原初のコンテンツや公式ドキュメントみたいなものは見つけられなかったので、ネット上のコンテンツをいい感じにまとめてくれていることを期待して、敢えてAI1の説明からも引用しますと、
RailsのFormObject(フォームオブジェクト)は、フォーム処理のために特化した「専用のオブジェクト」を用いて、複数モデルにまたがるデータ操作や入力バリデーションを整理・分離する設計パターンの一つである。
Railsのフォーム処理は通常、1つのActiveRecordモデルに結びつけて行うが、複数のモデルを扱う場合や、データベースに直接保存しないフォーム(例:検索フォーム、登録ウィザードなど)の場合、モデルの責務が曖昧になることがある。FormObjectはそれを解決するために使われ、ActiveModelモジュールをincludeした通常のRubyクラスとして定義される。
利点
- 複数モデルを1フォームで扱える:例として、UserとProfileを同時に登録できる
- バリデーションの分離:フォーム固有の検証をモデルとは別に定義できる
- コントローラの肥大化防止:ビジネスロジックをフォームクラスへ委譲するため、Controllerがシンプルになる。
- ActiveRecord風のAPI利用:ActiveModelを利用することで、valid?やerrorsなどActiveRecordと同様の書き方ができる
例えば、ブログの投稿フォームで記事: Postとタグ: Tagを同時に扱う場合だと、FormObjectを利用して以下のように記述することができます。
class PostForm
include ActiveModel::Model
attr_accessor :title, :content, :tag_names
validates :title, :content, presence: true
def save
return false if invalid?
ActiveRecord::Base.transaction do
post = Post.create!(title: title, content: content)
tags = tag_names.split(',').map { |t| Tag.find_or_create_by!(name: t.strip) }
post.tags = tags
end
true
end
end
上記の例ではまずActiveModel::Modelをincludeしていますが、FormObjectはActiveModelを使うことに意味があります。
RailsでのモデルといえばActiveRecordですが、ActiveModelはActiveRecordとは違い、DBと直接やり取りする機能を持ちません。
RailsなどのMVCアーキテクチャではよくビジネスロジックをどこに書くかでController fatかModel fatかという論争が繰り広げられていますが、DBとの接続機能をもたないActiveModelをincludeしたFormObjectをControllerとModelの間に置くことで、特にModel fatの課題を緩和することができそうですね。
FormObjectの使いどころ
今回のKaigiOnRailsでは入門FormObjectというタイトルでFormObjectの使いどころについてお話ししてくださいました。
講演では、こういうときにFormObjectを使うという「点」の解説記事はたくさんあるものの、より抽象的な概念としての使いどころを理解するのは難しいという課題を上げていました。
私も先のFormObjectとはの章をまとめていて、「で、FormObjectって結局なんのために使うの?」というのが掴みづらいと感じました。
一応のところの私の結論としては、適切に責任範囲を分割するため、と理解しましたが...うーん。
講演では改めてFormObjectの特徴として以下を挙げています。
- データベースに紐づかないRubyオブジェクト
- モデルと同じI/Fで、主にコントローラから呼ばれる
- 独自のライフサイクル処理を持てる
- ビューの状態を保持できる
これによりある側面ではServiceObjectといった他のレイヤよりもFormObjectが最適解となります。
では、それはどんな側面でしょうか。
FormObjectの目的とはずばり、Request、Controller、Model、Tableが1対1で対応し、ライフサイクル処理がCRUDで一様であることを期待するRailsWayの制約を回避することです。
なるほど。分かりやすいです。そして、全然違った。
これを分解すると、FormObjectの使いどころは以下のように導き出すことができます。
- 一度に操作するモデルの数がひとつではない
- モデルを操作しない
- 複数のモデルを操作する
- モデルのライフサイクル処理2のパターンがひとつではない
- アクションごとにライフサイクル処理を分ける
FormObjectの利点としてバラバラと列挙されているように見えていたものが、RailsWayの制約の回避という目的があると、必要十分に洗い出されたものとしっかり理解できますね。
FormObjectの是非
講演の内容ではFormObjectの是非については触れられていません。
ただ、今回FormObjectについて調べていく中でFormObjectに対する否定意見をいくつか見かけたので取り上げたいと思います。
- FormObjectの肥大化
- 責務の不明瞭化
- ロジックの重複
- 機能クレクレ
FormObjectの責任範囲についてきちんと理解せずになんとなくで導入してしまうと、乱雑なServiceObjectのような使い方になってしまうことは想像に難くありません。
そうなった場合そのFormObjectはいろんなロジックが詰め込まれ、責務が分からず、拡張性も保守性も損ないます。
また、なんでもFormObjectにしてしまうと、似たようなオブジェクトがあちこちに分散してしまったり、本来は自身で持つべきものですら過度に分割してしまったり、逆に全体管理が難しくなることも起きてしまうでしょう。
参照:
さいごに
仕事でアプリケーションでFormObject使ってるんだっけ?というのは気になるところで改めて調べてみました。
たしかに紐づくモデルがない場合などFormObjectが適切に使われていそうなものもありましたが、これはちょっとどうなんだろう?というのも結構ありそうでした。
個人ブログなのであまり詳細には書けないのですが...
- ActiveModelがもつメソッドをオーバーライドしているように見えてActiveModelの仕様に準拠していなかったり
- initializeでcontrollerのparamsをそのままごそっと渡していたり
FormObjectは非常に便利な反面、取り扱いには十分に気をつける必要があり、もし使うときはActiveModelについてきちんと理解しようと思いました。