はじめに
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にアクセスし、データの保存・変更・削除を行い、
その結果をController
やView
に返します。
返ってきた結果は、Controller
で整形・制御しView
に表示させたり、制御・整形せずそのまま表示したりします。
これが基本的なMVC
の流れですが、、、以下のようなことがあると思います。
- 一つの
View
で複数のModel
の属性を入力してもらい、DBを更新したい。 -
Model
やDBの制限とは別にView
側で制限をかけたい。 -
Controller
でのView
から受け取った情報の整形が複雑化してしまって、見辛くなった。
...
これらの実装をMVCだけで行おうとすると、Controller
かModel
に責務が偏ってしまいます。
これでは、コードが追いづらくなったり、影響範囲が大きくなったりしてしまいます。
そうすると、アプリの保守性が低いことになってしまうので、
新しい機能を追加したり、、、
修正したり、、、
DBの構造を変えたり、、
するときに大変苦労な思いをすることになってしまいます。
それらを防ぐための手法の一つとして、FormObject
という概念が存在します。
メリット・デメリット
FormObject
を使用する一番の目的は、責務を切り離すことだと思います。
Controller
の肥大化の原因のひとつである、View
からの情報を整形するロジックをFormObject
でカプセル化する事で、
Controller
の責務が一つ減ることになります。
これでController
は、肥大化せずに済みますし、単一責務の状態になるので依存関係が少なくなり、保守性は高くなります。
また、Modelの状態に関係ないロジックをModel
にかくと、Model
が肥大化してしまいます。そういったロジックをFormObject
に書くことで、
Model
の責務が一つ減り、肥大化を防ぐことができます。
このように、Controller
やModel
に偏りがちなロジックをFormObject
にまとめることで、過剰責務にならずに済みます!!
ただ、上記のことはデメリットにもなり得ると思います。
やたら滅多に、FormObject
にロジックを集約させると、今度はFormObjectが肥大化してしまいます。
これでは本末転倒です。。。
Controller
とModel
のView
に関するロジックを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に保存する際の制限です。
ただ、今回では検証している属性がFormObject
とModel
でかぶってしまっているので、
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
ここで、FormObject
は**Model
の生成・更新の役割を持っていいのかどうか**に疑問を持ちました。
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
の考えもあるのかなと感じています。