はじめに
2025/9/26にKaigi on Railsで下記のセッションを見た。 https://speakerdeck.com/joker1007/jin-gai-meteservicekurasunituitekao-eru-arurailskai-fa-zhe-no10nian
セッションはRails開発におけるServiceクラスの功罪を登壇者の経験を基に考察し、チーム開発での安易な使用に警鐘を鳴らす内容だった。 Serviceクラスと比較するとForm Objectは現実的な折衷案ではあるが、本質は「小手先のテクニックに逃げずにモデリングを頑張れ」というメッセージだと自分は受け取った。これには大いに賛同する。 また、チーム開発において開発者間でのアーキテクチャへの共通認識の形成とその維持が重要である(そして、それが非常に難しいという点)ことにも、強く共感できた。
表面的な解決策に過ぎないForm Objectではあるが、どうしても使用せざるを得ない場面もあるため、本記事で自分なりの考えを言語化しておきたい。 ここではForm Objectの責務、いつ使うか、Form Objectの課題について述べようと思う。実装については検索すればたくさん出てくるため、この記事では触れない。
Form Objectの責務
まず、Form Objectの責務とは何かを整理しておきたい。 スライド内で「諸説あると思うが」と述べられていた通り、自分も諸説あると考える。この「諸説」の存在が、開発者間で共通認識を形成するコストとなり、チーム開発への導入を難しくする一因となっている。 スライドでは、Form Objectは「パラメータハンドリング1」と「トランザクションコントロール」を扱うレイヤーとして紹介されていた。 これに対し、「2つある時点で単一責任の原則(Single Responsibility Principle)から外れているではないか」という指摘もあるだろう。自分はここが「諸説ある」所以だと考えており、Form Objectは「パラメータハンドリング」のみ行うべきという立場も存在する。 ここはチーム開発でForm Objectを採用する際の重要な論点の1つであり、導入前にチーム内で議論すべき点だ。
パターン1. パラメータハンドリング + 永続化(スライドで示されている考え方)
Form Objectがバリデーションからモデルの保存(トランザクション制御を含む)まで、一連の処理をすべて担当するパターンである。
-
Pros
- コントローラーのコードが
form.save
のように非常にシンプルになる(抽象化レイヤーとして機能する) - 特定のリクエストに関するロジックが1つのクラスにカプセル化されるため、ユースケース単位の自動テストが書きやすい
- コントローラーのコードが
-
Cons
- 複数の責務を持つことになる(これはRailsのActiveRecord自体にも向けられる批判である)
- 再利用性は低い(ただし、そもそもForm Objectは特定のリクエストと密結合しているため、再利用するケースはほぼ無いだろう)
パターン2. パラメータハンドリングのみ行う
Form Objectはバリデーションやデータ整形のみを行い、実際の保存処理は別途行うパターンである。
- Pros
- 責務が明確になる
- Cons
- 1つのリクエストを処理するために、パラメータハンドリングはForm Objectで、保存(トランザクションコントロール)はServiceクラス(できれば作りたくない)やControllerなどで行う必要がある
自分としてはパターン1を採用することが多い。なのでパターン2のPros/Consがあまり思いつかなかったことを正直に白状しておく。
パターン1を好む理由は、ActiveModel
を活用することで、よりRailsらしく書けるからだ。ただし、保存や更新を行わない検索フォームなどでForm Objectを活用する場合は、パターン2を採用することもあるだろう。
また、パターン1の責務をさらに拡張し、保存後の通知処理などもForm Objectに置きたい誘惑に駆られるケースもあるだろう。自分はなるべく避けるべきだと考えるが、場合によってはそういう判断もあり得る。
Form Objectをいつ使うか
では、いつForm Objectを使うか。 特に以下のような状況で有効だと考える。逆に言うと、下記に当てはまらない場合は使う必要がない可能性が高い。 なお、このリストが完全に網羅できているとは言えないため、もし他にも必要な状況があれば指摘いただけると幸いだ。
-
複数のモデルにまたがる操作が必要な場合
特定のリクエストで複数のモデルを同時に作成・更新するような処理を行うとき。 -
パラメータが複雑な場合
ネストされていたり、特別な加工や型変換が必要だったりする複雑なパラメータを処理する必要があるとき。 -
特定のリクエストやユースケースに固有のロジックがある場合
「この画面、このAPI呼び出しでだけ特定の処理やバリデーションを行いたい(or 行いたくない)」といった要件があるとき(ただし、ビジネスロジックをForm Objectに書くのは避けたい)。 -
トランザクションが必要な場合
複数のデータベース操作を一つのまとまった処理として成功または失敗させたいとき(前述の通り、1つのモデルしか触らないがトランザクション管理が必要なケースはあるので、1とは分けている)。
Form Object共通の課題
最後に、Form Objectに共通する課題を述べる。なお、チーム開発を前提としている。
チーム内での共通認識の形成と維持の難しさ
まず、繰り返しになるが、開発者間で共通認識を形成・維持するのにコストがかかる。
これはForm Objectに限らずapp
配下にディレクトリを増やす行為すべてに共通するが、Form Objectを「いつ、どのように使うか」という基準をチーム全体で一貫して保つのは難しい。
この共通認識を形成できていないと、コードベースに一貫性がなくなり、開発者の認知負荷(課題外在性負荷)が増すことになる。
本質的なモデル設計から逃げてしまう危険性
Form Objectは、複雑化したActiveRecord
モデルの問題を一時的に解決するように見えて、根本的な課題を隠蔽してしまうことがある。
これは登壇者のスライドでも指摘されている通りである。
モデルとForm Objectの責務の境界線
スライドではモデルとForm Objectの依存関係の話(大魔王のくだり)があったが、この他にも「このバリデーションはモデルとForm Objectのどっちに実装すべきか?」といった判断に迷うケースが頻繁に発生する。これも困りごとのひとつだろう。 自分の場合、基本的には「特定の画面やリクエストといったコンテキストでのみ必要な関心事」をForm Objectに寄せ、ビジネスロジックはモデルに寄せるようにしている。2これもチーム内で認識を合わせるべき事項のひとつだろう。
おわりに
Form Objectは、基本的には使わなくて済むならそれに越したことはない。 スライドではFat Modelの緩和策として挙げられていたが、個人的にはFat Modelならまだ良いほうで、世の中にはもっとひどいFat Controllerが存在する。Fat Controllerを放置しておくくらいなら、一旦(解決策として妥当な場合は)Form Objectを使ってThin Controllerにし、そこからModelの問題に向き合うというのは、かなり現実的な戦術だと言える。 なので手札の一つとして持っておくと良いだろう。