LoginSignup
6
4

More than 3 years have passed since last update.

FormObjectをうまく使いこなしたい

Last updated at Posted at 2020-12-16

はじめに

Rails Advent Calendar 2020 16日目の記事です!

業務で毎日、Railsを触れていいコードが書けるように精進しています。

そんな中、今年一扱いに苦戦した(今も苦戦している)FormObjectについて、自分が考えることを共有しておきたいと思います!!

FormObjectとは

そもそもFormObjectについて、「なにそれ????」と思う方もいると思うでの簡単に紹介します!

簡単に言うと、FormObjectは、Railsのデザインパターン設計手法の1つです。

MVC

Railsの基本設計は、MVC(Model, View, Controller)です。

Viewは、ユーザーからの入力を受け付けます。その情報をControllerにわたし、Controllerはその情報を整形・制御します。
そして、Controllerは整形・制御した情報をModelに対して渡してあげます。
Modelは、Controllerから受け取った情報をもとにDBにアクセスし、データの保存・変更・削除を行い、
その結果をControllerViewに返します。
返ってきた結果は、Controllerで整形・制御しViewに表示させたり、制御・整形せずそのまま表示したりします。

これが基本的なMVCの流れですが、、、以下のようなことがあると思います。

  1. 一つのViewで複数のModelの属性を入力してもらい、DBを更新したい。
  2. ModelやDBの制限とは別にView側で制限をかけたい。
  3. ControllerでのViewから受け取った情報の整形が複雑化してしまって、見辛くなった。 ...

これらの実装をMVCだけで行おうとすると、ControllerModelに責務が偏ってしまいます。
これでは、コードが追いづらくなったり、影響範囲が大きくなったりしてしまいます。

そうすると、アプリの保守性が低いことになってしまうので、

新しい機能を追加したり、、、
修正したり、、、
DBの構造を変えたり、、

するときに大変苦労な思いをすることになってしまいます。

それらを防ぐための手法の一つとして、FormObjectという概念が存在します。

メリット・デメリット

FormObjectを使用する一番の目的は、責務を切り離すことだと思います。

Controllerの肥大化の原因のひとつである、Viewからの情報を整形するロジックFormObjectでカプセル化する事で、
Controllerの責務が一つ減ることになります。
これでControllerは、肥大化せずに済みますし、単一責務の状態になるので依存関係が少なくなり、保守性は高くなります。

また、Modelの状態に関係ないロジックをModelにかくと、Modelが肥大化してしまいます。そういったロジックをFormObjectに書くことで、
Modelの責務が一つ減り、肥大化を防ぐことができます。

このように、ControllerModelに偏りがちなロジックをFormObjectにまとめることで、過剰責務にならずに済みます!!

ただ、上記のことはデメリットにもなり得ると思います。

やたら滅多に、FormObjectにロジックを集約させると、今度はFormObjectが肥大化してしまいます
これでは本末転倒です。。。
ControllerModelViewに関するロジックをFromObjectに任せて、責務を切り離すことを目的に使用しているにもかかわらず、
FormObjectの責務がでかくなってしまって、そこの保守性が低くなるのは良くありません。

そのため、使用する前はしっかりとFormObjectの責務を明確にしてから使用しましょう!

FormObjectの責務とは???

FormObjectがなんなのか大体わかってきました。

かなり便利なFromObjectですが、、、いろいろな記事をみて、実際に書いていく中で、ふと思ったことがあります。。。

「ここってFomObjectでやるべき???」
「ControllerやModelのロジックとかぶってない???」
「FormObjectの責務って、、、結局どこまで持てばいいの???」

これらを思ったきっかけを実際のコードを例に説明していきたいと思います。

FormObjectのvalidate

FormObjectを使用するとき、ActiveModel::ModelをincludeしてればModel同様にvalidateを書けることができます。
例えば、記事の登録でタグ情報も入力するフォームがあったとします。

フォームでは、記事のタイトルと内容、タグの名前が必須であった場合、以下のようにかけます。

class ArticlePostForm
  include ActiveModel::Model
  attr_accessor :article_body, :article_title, :tag_name

  validates :article_body, presence: true
  validates :article_title, presence: true
  validates :tag_name, presence: true
end

これでFormの入力に対して制限を設けることができました。ただ、記事とタグのModelにも以下のようなvalidateが設けてありました。

class Article < ApplicationRecord
  validates :title, presence: true, uniqueness: true
  validates :body, presence: true
  validates :is_display, inclusion: [true, false]
end

class Tag < ApplicationRecord
  validates :name, presence: true, uniqueness: true
end

FormObjectでのvalidateとModelでのvalidateでは、意味は違います。
FormObjectでのvalidateでは入力された値に対する制限なのに対して、ModelでのvalidateではDBに保存する際の制限です。

ただ、今回では検証している属性がFormObjectModelでかぶってしまっているので、

FormObjectのvalidateは必要なのか?

という疑問があります。

FormObjectでのsave・update

記事を見ているとよく見かけるものです。
以下のようにFormObjectでActiveRecordの作成を行うsaveメソッドがあります。

class ConractForm
  include ActiveModel::Model
  attr_accessor :first_name, :last_name, :email, :body

  def save
    return false if invalid?

    contact = Contact.new(first_name: first_name, last_name: last_name, email: email, body: body)
    if contact.save
      true
    else
      errors.add(:base, contact.errors.full_messages)
      false
    end
  end
end

Controllerでは、以下のように書けるはずです。

class ContactController < ApplicationController
  def new
    @contact = ContactForm.new
  end

  def create
    @contact = ContactForm.new(set_params)
    if @contact.save
      flash.now = 'お問い合わせを承りました'
      redirect_to :contacts
    else
      flash.alert = '送信に失敗しました'
      render :new
    end
  end

  private

  def set_params
    params.require(:contact).permit(:first_name, :last_name, :email, :body)
  end
end

ここで、FormObjectModelの生成・更新の役割を持っていいのかどうかに疑問を持ちました。
FormObjectは、入力に対してロジックを持たせるのが一般的だと思います(間違っていたらすいません。。。)

ここで僕はFomObjectに対しての責務で、

「Viewの入力に対しての制御のみでModelの生成は別」と「validateをかけているからModelの生成までがいい」

の2通りの考え方できると思ったので、どちらに寄せるべきなのかを迷いました。

FormObjectのエラーハンドリング

Modelの生成において、validateによって発生したエラーをFormObjectで取り扱うかどうかがどうなのか疑問に思いました。

ModelのエラーとFormObjectのエラーは分けたほうがいいのか、一緒にすべきなのかが一番悩ましいところです。。。

(ここは僕もまだ考えがまとまっていないので、知見が増えたらまた共有します。)

def save
    # ここはFormObjectの入力のエラー
    return false if invalid?

    # ここはModel生成時のエラー
    contact = Contact.new(first_name: first_name, last_name: last_name, email: email, body: body)
    if contact.save
      true
    else
      errors.add(:base, contact.errors.full_messages)
      false
    end
  end

まとめ

FormObjectに対しての責務は、その都度考え、その責務以上のことをさせないように実装することが大事だと思っています。

ただ、僕自身もFormObjectを完璧に把握・理解しているわけではなく、他にも設計思想があるので、そこらへんと組み合わせた
FormObjectの考えもあるのかなと感じています。

参考サイト

Form Object という選択肢を検討してみる

【Rails】FormObjectを使ってほしい

Railsのデザインパターン: Formオブジェクト

【Rails】Form Objectを使ってModelに依存しないFormを作成する

6
4
0

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
6
4