1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RailsのFormオブジェクトを触ったので、備忘録として残していきたい

Last updated at Posted at 2023-10-31

はじめに

インターン先のrails開発でFormオブジェクトなるものに触れる機会があったので、備忘録として残す

Formオブジェクトとは

controllerやviewに入り混じるメソッド群を単一のクラスにカプセル化することが可能で、Formからのリクエストデータに対して採用されるRailsのデザインパターン、リファクタリング技法のひとつです。

最初に軽く、今回の実装におけるFormオブジェクトの定義について説明します。
Formオブジェクトはmodel層として定義され、controller層からリスエストパラメータを取得し、そのデータを加工、検証、保存するための責務を有します。

ユースケース

一般的にFormオブジェクトのユースケースは以下の通りです。

ユーザー認証

例えば、password確認用(password_confimation)の入力フォームはmodelと直接関係のない属性です。こういった属性に対するロジックをmodelに書き続けた結果出来上がるのが、ファットモデルだと思っています。

controller側
フォームからの入力値をstrong parameterを通して受け取り、Formオブジェクトにデータを渡す

user_controller.rb
@form = UserRegistrationForm.new(user_registration_params)
@form.save

Formオブジェクト側
attr_accessorとして定義された属性に、controllerから渡されたハッシュデータをそれぞれセットすることで、変数のgetterとsetterを自動で生成している

form/user_regisration_form.rb
class UserRegistrationForm
  include ActiveModel::Model

  attr_accessor :email, :password, :password_confirmation, :nickname,

  validates_confirmation_of :password

  def initialize(attributes = {})
    @email = attributes[:email]
    @password = attributes[:password]
    @password_confirmation = attributes[:password_confirmation]
    @nickname = attributes[:nickname]
  end

  def save
    return false unless valid?

    User.create(
      email: email,
      password: password,
      nickname: nickname,
    )
  end
end

上記例では、model属性に関するvalidationはmodelクラスで行い、passwordの一致に関してはFormオブジェクトクラスで行っています。

他にも以下のようなユースケースが挙げられます。

  1. DB操作の対象にならないデータの処理
  2. ネストされた属性
  3. カスタムバリデーション

などです。

メリット

ここからはFormオブジェクトを用いるメリットについて説明しようと思います。

1. ビジネスロジックの隔離

Formオブジェクトを使用することで、コントローラからビジネスロジックを分離できます。フォームオブジェクトは、フォームデータのバリデーションやデータベースへの保存など、フォームに関連する操作をカプセル化するのでcontrollerがシンプルとなり、再利用性が上がります。

2. テスト容易性

当たり前ですが、Formオブジェクトは独立してテストできるため、テストケースを書くことが容易です。Formオブジェクトのメソッドに対してテストを実行し、フォームのバリデーションやデータ処理の正確性を確認できます。

3. フォームデータの整形

Formデータを整形して、DBモデルに適した形式に変換することができます。これにより、Formデータの取り込みとデータベースへの保存がスムーズに行えます。

今回の利用ケース

※ 実際の実装やテーブル名はぼかします

内容

「人物プロフィールを出身高校、出身大学、出身企業といった一貫性のあるデータを持つ」という実装

DB構成

image.png

なぜFormオブジェクトを用いるのか

今回の対象である出身高校、出身大学、出身企業の履歴を管理する各xxx_backgroundsテーブルはカラムが全て対称である。
つまり、1経歴あたりUI上入力する内容は、**どの企業(高校ort大学)**か、いつからなのか、いつまでなのかであり、共通部分として考えられる。

MVCアーキテクチャを考えると、共通のFormから入力されたパラメータを捌くperson_backgrounds_controller.rbを作成する必要があります。
ただ、このcontrollerには直接関係のあるDB(model)がありません

ここの共通の処理や入力パラメータ各modelに基づくDbとの連携の責務をFormオブジェクトとしてクラスに切り出すことにしました。

実装

以下に実装を示します

person_background_form.rb
class PersonBackgroundForm
  include ActiveModel::Model

  attr_accessor :person_id, :person_type, :company_id, :university_id, :high_school_id,
  :started_at, :ended_at

  def initialize(attributes = { })
    super(attributes)
    attributes.each do |name, value|
      send("#{name}=", value) if respond_to?("#{name}=")
    end
  end

  def save
    attributes = { }
    attributes[:started_at] = started_at.presence
    attributes[:ended_at] = ended_at.presence

    begin
      case person_type
      when 'company'
        attributes[:company_id] = company_id
        person.person_company_backgrounds.create!(attributes)
      when 'university'
        attributes[:university_id] = university_id
        prerson.person_university_backgrounds.create!(attributes)
      when 'high_school'
        attributes[:university_id] = university_id
        prerson.person_high_school_backgrounds.create!(attributes)
      else
        return {”失敗時の処理”}
      end
    rescue ActiveRecord::RecordInvalid => e
      return { "例外時の処理" }
    end

    { status: true }
  end

  private

  def person
    @person ||= Person.find(person_id)
  end
end

以下で上記実装について説明していく

  1. PersonBackgroundFormクラスは、ActiveModel::Modelモジュールをインクルードしており、Active Recordモデルの代わりにデータのバリデーションと保存に使用できるFormオブジェクトを作成する
    → これにより、obj.create!の際のvalidationを実行できる

  2. attr_accessorメソッドを使用して、このFormオブジェクトの属性を設定します。これらの属性はコード内で使用され、フォームから送信されたデータを格納する

  3. initializeメソッドは、Formオブジェクトが作成されるときに属性を設定し、受け取ったハッシュ内の属性とその値をFormオブジェクトの属性に設定する

  4. saveメソッドは、フォームから送信されたデータを受け取り、それをデータベースに保存する。このメソッド内で、person_typeの値に応じて異なるデータベーステーブルにデータを保存する

  5. 例外処理が行われており、データの保存が失敗した場合にエラーメッセージを返します。保存に失敗した場合、{ status: false, errors: ["error"] } の形式のハッシュを返す

また、このFormオブジェクトを以下のようにcontroller側で使用し、各parameterを渡す

person_background_controller.rb
def create
    @person_background_form = PersonBackgroundForm.new(person_background_params)
    result = @person_background_form.save

    if result == "成功"
      redirect_to xxxx_path, notice: '役員経歴を登録しました'
    else
      flash.now[:alert] = ”失敗だよ”
      render :new
    end
  end

  private

  def person_background_params
    hogehoge
  end

上記のようにFormオブジェクトクラスをcontroller側で呼ぶことで、saveの責務を別クラスに切り出すことができる

本実装でのメリット

  1. 単一責任の原則
    a. この実装では、フォームオブジェクトがビジネスロジックを担当しており、controllerやviewから分離されているので、Formオブジェクトはデータのバリデーションとデータベースへの保存に焦点を当て、単一の責任を持つ。

  2. テスト容易性
    b. Formオブジェクトは単体テストが容易である。フォームオブジェクトの各メソッドを独立してテストし、データのバリデーションや保存が期待どおりに動作することを確認することができる。
    → 従来通りの実装であれば、各3つのcontrollerに実装が分かれてしまうため、各controller specにテストコードを書く必要があるが、今回ビジネスロジックは全てFormオブジェクト内にカプセル化されているので、テスト用意性を担保できた

  3. 可読性
    c. ビジネスロジックがフォームオブジェクト内に集中しているため、controllerやviewのコードはシンプルで、可読性が向上する。

最後に

今回はRuby on Railsのデザインパターン、リファクタリング技法の一つである、Formオブジェクトでの実装を振り返った。
今まで何気なく書いていたコードでも共通部分をFormオブジェクトのクラスとしてカプセル化することでテスト見通しがよくなったり、亜k毒性の高いコードが書けるようになると感じた。

これからも取捨選択をしながら、この便利なパターンを採用して、可読性の高いコードを書けるように精進していきたい

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?