Published on

〜フォーム保存処理の責務が曖昧になるの厳しいって〜Railsで単体のモデルに依存しない保存処理を実装するためのFormオブジェクトパターンを実例付きで解説❗️

Authors

この記事は ex-crowdworks Advent Calendar 2024の19日目の記事です。

はじめに

今年、株式会社クラウドワークスを退職した@nisyuuです。News Connectをよく聞いています。 エンジニアとしてクラウドワークステック(旧クラウドテック)というフリーランスと企業をマッチングするエージェントサービスを開発していました。

Railsエンジニアの皆様は、フォームはどのように実装しているでしょうか? 単体のモデルに紐づく保存処理については、シンプルに実装できるかもしれませんが、複数モデルにまたがるような保存処理をするとバリデーションなどを含む処理の責務が曖昧になってしまいます。 このような場合、Formオブジェクトを使うことで責務を分離し、可読性を上げることができます。

Formオブジェクトパターンとは

Formオブジェクトパターンは、Webアプリケーションのフォーム入力を処理する際に使用される設計パターンです。このパターンでは、フォームの入力データやそのバリデーション、データの保存処理などを専用のクラスに分離します。これにより、コントローラやモデルの役割が明確化し、コードの可読性と保守性が向上します。

特徴

  • フォーム入力を専用のクラスに切り出す
  • 複数のモデルにまたがる処理を一元管理
  • バリデーションやビジネスロジックを整理

利用例

  • ユーザー登録フォームで複数のモデルを扱う場合
  • フォームが複雑でバリデーションが多岐にわたる場合

Formオブジェクトパターンのメリットとデメリット

メリット

  • コントローラの簡潔化
    • コントローラのコードが短くなり、単純な処理に集中できる
  • 複雑なフォームの管理
    • 複数のモデルにまたがる処理を一つのクラスにまとめられる
  • テストの容易化
    • ビジネスロジックやバリデーションをFormオブジェクトでテスト可能
  • 再利用性の向上
    • 共通するフォームロジックを一箇所に集約可能

デメリット

  • コード量の増加
    • 新たなクラスを作成するため、初期の実装コストが上がる
  • 適用範囲の判断が難しい
    • 単純なフォームでは過剰設計になる可能性がある
  • Railsの標準機能を超える学習コスト
    • RailsのActiveRecordに慣れている開発者にとっては新しい概念の学習が必要

RailsでのFormオブジェクトパターンの実装例

以下は、ユーザー登録フォームを例にFormオブジェクトを実装する方法です。このフォームでは、UserモデルとProfileモデルのデータを一度に保存します。

1. Formオブジェクトの作成

まず、新しいクラスを作成します。

# app/forms/user_registration_form.rb
class UserRegistrationForm
  include ActiveModel::Model

  attr_accessor :name, :email, :password, :profile_bio

  validates :name, presence: true
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 6 }
  validates :profile_bio, presence: true

  def save
    return false unless valid?

    ActiveRecord::Base.transaction do
      user = User.create!(name: name, email: email, password: password)
      Profile.create!(user: user, bio: profile_bio)
    end
    true
  rescue ActiveRecord::RecordInvalid
    false
  end
end

2. コントローラでの使用

コントローラでFormオブジェクトを使用します。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @form = UserRegistrationForm.new
  end

  def create
    @form = UserRegistrationForm.new(user_registration_form_params)

    if @form.save
      redirect_to root_path, notice: 'ユーザー登録が完了しました'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def user_registration_form_params
    params.require(:user_registration_form).permit(:name, :email, :password, :profile_bio)
  end
end

3. ビューの作成

フォームを作成します。

<!-- app/views/users/new.html.erb -->
<%= form_with model: @form, url: users_path do |f| %>
  <div>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>

  <div>
    <%= f.label :profile_bio %>
    <%= f.text_area :profile_bio %>
  </div>

  <div>
    <%= f.submit '登録' %>
  </div>
<% end %>

まとめ

Formオブジェクトパターンは、複雑なフォームロジックを管理しやすくする便利な設計パターンです。 複数のモデルを扱う場合や、バリデーションが複雑な場合に威力を発揮するのでフォーム実装の設計で検討してみると良いかもしれません。