はじめに
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の考えもあるのかなと感じています。