Madogiwa Blog

主に技術系の学習メモに使っていきます。

RubyOnRails:FormObjectを使って複雑なフォームの処理を良い感じに実装するメモ✍

みなさん、こんにちは!まどぎわです(・∀・)
今回は、Railsデザインパターン(?)の一つのFormObjectについて学んだので、使い方とかをメモしておきます✍

FormObjectってなに?

FormObjectとは、ActiveModelincludeしたClassにフォームで扱うプロパティをもたせたものです。

私は実際に使ってみて、下記のようなメリットを感じました!!

  • 複数モデルにまたがったバリデーション等、記載に箇所に迷う実装をスッキリ書ける。
  • Controller側の実装がスッキリして、コードの見通しがよくなる。

ちょっと、具体的な例と合わせて見ていこうと思います👀

FormObjectってどういうときに使う?

例えば、タイトルと本文とタグ(10件まで)登録するブログサービスのようなものがあったとします。
Blogに紐づくTagの個数チェックや、タグを含めたブログの登録処理等、実装箇所に迷う部分が多かったですが下記のような実装(イメージ)としてみました。。。

この状態ではコントローラーの中にエラーチェックがあったり、タグのインスタンスを作る処理があったり、トランザクションを貼って複数モデルにまたがった作成処理を行っていたり等、見通しが悪く、またコントローラーに書きたくないような処理があって、ごちゃちゃしているように感じますね(;・∀・)

class BlogsController < ApplicationController
  TAGS_LIMIT = 10
  
  def create
    @post = BlogPost.new(blog_post_form)
    ActiveRecord.transaction do
      check_up_to_limit_tags_can_be_set!
      blog = Blog.create!(title: blog_params[:title], body: blog_params[:body])
      build_tags.each do |tag|
        tag.save!
        Tagging.create!(blog: blog, tag: tag)
      end
    end
    redirect_to blogs_path
  rescue ActiveRecord::RecordInvalid
    render :new
  end

  private
  
  def check_up_to_limit_tags_can_be_set!
    return if tags.length <= TAGS_LIMIT
    @post.errors.add(:tags, 'は、10個まで設定可能です。')
  end
  
  def build_tags
    blog_params[:tags].map { |tag| Tag.new(name: tag) }
  end
end

複数モデルにまたがったエラーチェックやモデルの生成等、ちょっと複雑な機能だと結構ありがちで、実装を迷う部分な気がするのですが、こういうときにFormObjectを使うと結構スッキリかけます🙌

FormObjectを使ってリファクタリングしてみる

先程話した例をFormObjectを使ってリファクタリングしてみると下記のような感じになります!!
BlogPostFormに実装に迷った処理が集約されて、コントローラー内もスッキリし、フォーム側に登録処理を寄せることで、複数モデルの登録も違和感無い形になっているので良さそうに見えますね👀✨

class BlogsController< ApplicationController
  def create
    @post = BlogPostForm.new(blog_params)
    post.save!
    redirect_to blogs_path
  rescue ActiveRecord::RecordInvalid
    render :new
  end
end

class BlogPostForm
  TAGS_LIMIT = 10
  include ActiveModel::Model
  attr_accessor :title, :body, :tags
  validates :title, :body, presence: true
  valudate :up_to_10_tags_can_be_set
  
  def save!
    raise ActiveRecord::RecordInvalid if invalid?
    ActiveRecord.transaction do
      blog = Blog.create!(title: title, body: body)
      build_tags.each do |tag|
        tag.save!
        Tagging.create!(blog: blog, tag: tag)
      end
    end
  end
  
  private
  
  def build_tags
    tags.map { |tag| Tag.new(name: tag) }
  end
    
  def up_to_limit_tags_can_be_set
    return if tags.length <= TAGS_LIMIT
    errors.add(:tags, 'は、10個まで設定可能です。')
  end
end

FormObjectをもっと知る

FormObjectについて、すごく分かりやすく記載されてる記事です👀
※多大に参考にさせて頂きました・・・!🙇‍♂️

tech.medpeer.co.jp

FormObjectのためのGemもあるみたいです👀

github.com

おわりに

今回はFormObjectについて、少し整理してみました。
このようなデザインパターンのような知識を学ぶと自分の実装の引き出しが増える感じがして良いですね🙌
これからも学んでいきたみが強い・・・!!