Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

KaigiOnRails-2025 day1 入門FormObjectの話を聞いてきた

ogp

2025年9月26日〜27日に開催されたKaigi on Rails 2025へ参加してきました。今回はday1で行われた小川原修(@expajp)さんの「入門FormObject」の発表を聞いてきたので、学んだ内容をまとめました。

セッション概要

FormObjectってよく使われているけれど、概念や使いどころをうまく説明できる人って案外少ないんじゃないでしょうか。

「なんとなく使っているけど、本当にここで使うべきなの?」 「他の人に説明を求められても、うまく答えられない。」 「Rails Wayとの使い分けがよくわからない。」

そんな経験、ありませんか。

FormObjectは確かに便利なパターンですが、その一方で「いつ使うべきか」「なぜ必要なのか」といった根本的な部分の理解が難しく、多くの開発者が悩むポイントでもあります。

小川原さんの発表は、FormObjectの本質的な特徴から具体的な使いどころのパターンまで体系的に解説されていました。また、現場にFormObjectが溢れている理由についても初級者にも分かりやすく説明されており、「FormObjectについてもっと知りたい」と思っていた方には非常に参考になるセッションでした。

FormObjectとは何か

なぜFormObjectが必要なのか?

FormObjectを理解するために、まずは「なぜこのパターンが生まれたのか」から見ていきましょう。

通常のRailsアプリケーションでは、以下のようなシンプルな構造で開発を進めます:

# 1. モデル(app/models/user.rb)
class User < ApplicationRecord
  validates :email, presence: true
  validates :name, presence: true
end
# 2. コントローラ(app/controllers/users_controller.rb)
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to users_path, notice: 'ユーザーを作成しました'
    else
      render :new
    end
  end
end

この方法は、シンプルなCRUD操作では完璧に機能します。しかし、実際の開発では以下のような場面に遭遇することがあります:

ケース1: お問い合わせフォーム

  • データベースに保存せず、メール送信だけしたい
  • でも、バリデーションやエラー表示は通常のフォームと同じように使いたい

ケース2: 複雑な登録フォーム

  • ユーザー情報、プロフィール情報、設定情報を一度に登録したい
  • 複数のテーブルにまたがるが、ユーザーには1つのフォームとして見せたい

ケース3: 文脈に応じたバリデーション

  • 新規登録時は「利用規約への同意」が必須
  • プロフィール編集時は「利用規約への同意」は不要
  • 同じUserモデルだが、場面によってバリデーションを変えたい

これらのケースでは、通常のActiveRecordモデルだけでは対応が困難です。そこで登場するのがFormObjectです。

FormObjectの基本的な考え方

FormObjectは、「フォームの要件に特化したオブジェクト」です。データベースのテーブル構造に縛られることなく、フォームが必要とする機能だけを持ったオブジェクトを作ることができます。

# FormObjectの例
class ContactForm
  include ActiveModel::Model  # バリデーションや属性の一括代入といった ActiveRecord の一部機能を利用できるようにする
  
  attr_accessor :name, :email, :message
  validates :name, :email, :message, presence: true
  
  def save
    return false if invalid?
    # データベースには保存せず、メール送信のみ
    ContactMailer.send_message(name, email, message).deliver_later
    true
  end
end

このFormObjectを使えば、コントローラからは通常のモデルと同じように扱えます:

def create
  @form = ContactForm.new(contact_params)
  if @form.save  # 通常のモデルと同じ書き方
    redirect_to root_path, notice: 'お問い合わせを送信しました'
  else
    render :new  # エラー表示も同じ
  end
end

FormObjectの4つの特徴

発表では、FormObjectを以下の4つの特徴で定義していました:

  1. データベースに紐付かないRubyオブジェクト
  2. モデルと同じインターフェース(以下I/F)で、主にコントローラから呼ばれる
  3. 独自のライフサイクル処理を持てる
  4. ビューの状態を保持できる

※「ライフサイクル処理」とは、発表者の小川原さんが使用していた呼称で、バリデーション(入力値の検証)やコールバック(保存前後の処理)など、オブジェクトの生成から保存までの一連の処理のことを指しています。

それぞれの特徴について、具体例とともに詳しく見ていきましょう。

1. データベースに紐付かないRubyオブジェクト

通常のActiveRecordモデルとは異なり、FormObjectはデータベースのテーブルと直接対応していません。

# 通常のActiveRecordモデル
class User < ApplicationRecord  # データベースのusersテーブルと対応
  validates :email, presence: true
end

# FormObject
class UserNewForm
  include ActiveModel::Model  # ActiveRecordではなくActiveModel
  
  attr_accessor :email, :name  # 属性を明示的に定義
  validates :email, :name, presence: true
end

ActiveRecordとActiveModelの違い

  • ActiveRecord: データベースのテーブルと1対1で対応するモデル。saveすると自動的にデータベースに保存される

  • ActiveModel: データベースに紐付かないが、バリデーションやモデルと同様のインターフェースなどRailsのモデル機能を使えるようにするモジュール

この特徴により、データベースの構造に縛られることなく、フォームの要件に合わせて自由に属性を定義できます。例えば、確認用のパスワードフィールドや、複数のモデルにまたがる情報を1つのオブジェクトで扱うことが可能になります。

2. モデルと同じI/Fで、主にコントローラから呼ばれる

FormObjectは、ActiveRecordモデルと同じメソッド(newsavevalid?など)を持つため、コントローラから見ると通常のモデルと同じように扱えます。

class UsersController < ApplicationController
  def create
    @form = UserNewForm.new(user_params)
    
    if @form.save  # ActiveRecordモデルと同じように使える
      redirect_to users_path, notice: 'ユーザーを作成しました'
    else
      render :new  # バリデーションエラーも同じように扱える
    end
  end
end

これにより、既存のコントローラのコードを大きく変更することなく、FormObjectを導入できます。Railsの「設定より規約」の思想を保ちながら、より複雑な要件に対応できるのです。

3. 独自のライフサイクル処理を持てる

FormObjectでは、そのフォーム特有のバリデーションやコールバックを定義できます。

class UserRegistrationForm
  include ActiveModel::Model
  
  attr_accessor :email, :password, :password_confirmation, :terms_accepted
  
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 }
  validates :password_confirmation, presence: true
  validates :terms_accepted, acceptance: true  # フォーム特有のバリデーション
  
  validate :passwords_match
  
  before_save :normalize_email
  
  def save
    return false if invalid?
    
    user = User.create!(
      email: email,
      password: password
    )
    
    # 登録完了メールの送信など、このフォーム特有の処理
    UserMailer.welcome_email(user).deliver_later
    true
  end
  
  private
  
  def passwords_match
    return if password == password_confirmation
    errors.add(:password_confirmation, 'パスワードが一致しません')
  end
  
  def normalize_email
    self.email = email.downcase.strip
  end
end

このように、フォームの文脈に特化したバリデーションや処理を組み込むことで、より適切な責務分離が実現できます。

4. ビューの状態を保持できる

FormObjectは、バリデーションエラーが発生した際に入力値を保持し、ビューで再表示できます。

<!-- app/views/users/new.html.erb -->
<%= form_with model: @form, url: users_path do |f| %>
  <% if @form.errors.any? %>
    <div class="error-messages">
      <% @form.errors.full_messages.each do |message| %>
        <p><%= message %></p>
      <% end %>
    </div>
  <% end %>
  
  <%= f.label :email %>
  <%= f.email_field :email %>  <!-- エラー時も入力値が保持される -->
  
  <%= f.label :password %>
  <%= f.password_field :password %>
  
  <%= f.submit '登録' %>
<% end %>

ActiveModelの機能により、通常のActiveRecordモデルと同じようにフォームヘルパーと連携し、ユーザビリティの高いフォームを構築できます。

これらの特徴を組み合わせることで、FormObjectは「フォームの要件に特化しつつ、Railsの規約に従った」オブジェクトとして機能するのです。

他のレイヤとの違い

FormObjectと似た役割を持つレイヤとの比較も示されました。

レイヤ モデルと同じI/F 入力バリデーション ビジネスロジック処理 ビューの状態保持
FormObject
Application Model ×
View Model × × ×
Interactor - ×
Service Object × × ×

FormObjectの使いどころ

Rails Wayの恩恵とその前提

Rails Wayは、以下の3つの前提に割り切ることで、驚くべき開発効率を実現しています:

  • リソース(R) - コントローラ(C) - モデル(M) - テーブル(T)がすべて1対1に対応
  • 一度に行うのはちょうどひとつのモデル操作でよい
  • ライフサイクル処理はひとつのパターンのみでよい

この「割り切り」により、Railsは以下のような大きな恩恵をもたらします:

開発速度の劇的な向上

rails generate scaffold User name:string email:string 一行で、完全に動作するCRUD機能が完成します。コントローラ、モデル、ビュー、ルーティング、マイグレーションファイルまで、すべてが一貫した構造で自動生成されます。

学習コストの大幅削減

一度Railsの規約を覚えれば、どのRailsプロジェクトでも同じパターンで開発できます。新しいプロジェクトに参加しても、ファイル構造やコードの書き方が予測でき、すぐに生産性を発揮できるのです。

チーム開発での一貫性

誰が書いても同じような構造になるため、コードレビューやメンテナンスが容易になります。「設定より規約」により、細かな設定で悩む時間を削減し、ビジネスロジックに集中できます。

メンテナンスの容易さ

予測可能な構造により、バグ修正や機能追加の際に「どこを見れば良いか」が明確です。新しいメンバーでも、既存のコードを理解しやすく、安心して変更を加えることができます。

しかし、現実はそう単純ではない

この素晴らしいRails Wayの恩恵を受けられるのは、上記の3つの前提が成り立つ場合です。しかし、実際のWebアプリケーション開発では、これらの前提が成り立たない場面が出てきます。

それは決してRails Wayが悪いのではありません。要件が複雑になれば、シンプルな前提だけでは対応しきれないのは自然なことです。

3つの使いどころパターン

FormObjectが必要になるのは、上記の仮定の「ひとつ」が崩れるときです:

パターン① モデルを操作しない

例:メールフォーム

class FeedbackForm
  include ActiveModel::Model
  
  attr_accessor :title, :body
  validates :title, :body, presence: true
  
  def save
    return false if invalid?
    SomeMailer.feedback(title, body).deliver_later
    true
  end
end

データベースへの保存は行わず、メール送信のみを行うケースです。

パターン② 2個以上のモデルを操作する

例:会社・社長・従業員を一画面で登録

class CompanyRegistrationForm
  include ActiveModel::Model
  
  attr_accessor :company_name, :company_address, :president_name, :employee_names
  
  def save
    ActiveRecord::Base.transaction do
      company = Company.create!(name: company_name, address: company_address)
      president = User.create!(name: president_name, role: 'president')
      employees = employee_names.map { |name| User.create!(name: name, role: 'employee') }
      
      company.president = president
      company.employees = employees
      company.save!
    end
  end
end

複数のモデルを一度に操作する必要がある場合です。

パターン③ ライフサイクル処理をアクションごとに分ける

例:ユーザの作成と編集でバリデーション分け

class UserNewForm
  include ActiveModel::Model
  attr_accessor :screen_name, :name
  validates :screen_name, :name, presence: true
  # 新規作成時はscreen_nameが必須
end

class UserEditForm
  include ActiveModel::Model
  attr_accessor :name
  validates :name, presence: true
  # 編集時はscreen_nameは変更不可のためバリデーション不要
end

同じモデルでも、アクションによって異なるバリデーションやライフサイクル処理が必要な場合です。

なぜ現場にFormObjectが溢れているのか

現代のWebアプリのUX要求

現代のWebアプリケーションでは、「複雑な操作をワンタップで実施したい」というユーザ要求が高まっています。

例えば、ECサイトの購入処理では: - 在庫を減らす - 注文を作成する - 決済する - 決済記録を作成する - 通知を送信する

これらの処理を「ワンクリック購入」として一度に実行する必要があります。

UXが売上に与える影響

発表では、コーネル大学の調査(2023年)が紹介されました:

ワンクリック購入に登録した顧客は、平均支出額・購入頻度・購入点数がいずれも増加し、サイトへの訪問回数・ページ閲覧回数・滞在時間も増加していた

このように、UXの改善が直接売上に影響する事例があるため、複雑な操作をシンプルなインターフェースで提供する必要性が高まっています。

実装への影響

複雑な操作をワンタップで実施できるようにすると:

  • 操作が複雑になると → 必要なデータが増え、一度に操作するモデル数が増える
  • ワンタップで実施できるようにすると → 同じモデルを別々の箇所から操作する可能性が高まる

いずれもFormObjectの使いどころに該当し、この傾向は時間とともに高まっていくため、現場にFormObjectが溢れているのです。

FormObjectとの向き合い方

理解して使うことの重要性

FormObjectは確かに理解が難しいパターンですが、Railsで仕事をするなら向き合わなくてはならない存在です。

重要なのは「なんとなく」で使うのではなく、以下を理解することです:

  • なぜFormObjectが必要なのか(Rails Wayの制約を超える必要がある)
  • どのパターンに該当するのか(0個、2個以上、ライフサイクル分離)
  • 適切な責務の分離(必要以上に複雑にしない)

「巨人の肩に乗る」姿勢

発表の最後で印象的だったのは、「『なんとなく』で使わず『巨人の肩に乗る』姿勢を忘れずに」というメッセージでした。

Railsは「簡単」と紹介されることが多いですが、簡単な部分だけで食っていけるほど甘くはありません。しかし、「レールの伸ばし方」は先人が検討して残してくれており、AIもそれらを学習しています。

FormObjectも、先人たちが積み重ねてきた知見の一つです。その背景や理由を理解して使うことで、より良いコードを書けるようになるでしょう。

まとめ

今回の発表を通じて、FormObjectについて以下のことを学びました:

  • FormObjectの使いどころは、Rails Wayの仮定に反するとき
  • 3つのパターン(0個、2個以上、ライフサイクル分離)に分けて考えられる
  • 現代のWebアプリのUX要求により、FormObjectの必要性は高まっている
  • 「なんとなく」ではなく、理解して向き合うことが重要

FormObjectは難しいパターンですが、その背景と使いどころを理解することで、より適切に活用できるようになります。現場でFormObjectに出会ったときは、今回学んだ3つのパターンのどれに該当するかを考えてみてください。

小川原さんの分かりやすい解説のおかげで、FormObjectへの理解が深まりました。ありがとうございました!

参考リンク


Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com