はじめに
インターン先のrails開発でFormオブジェクトなるものに触れる機会があったので、備忘録として残す
Formオブジェクトとは
controllerやviewに入り混じるメソッド群を単一のクラスにカプセル化することが可能で、Formからのリクエストデータに対して採用されるRailsのデザインパターン、リファクタリング技法のひとつです。
最初に軽く、今回の実装におけるFormオブジェクトの定義について説明します。
Formオブジェクトはmodel層として定義され、controller層からリスエストパラメータを取得し、そのデータを加工、検証、保存するための責務を有します。
ユースケース
一般的にFormオブジェクトのユースケースは以下の通りです。
ユーザー認証
例えば、password確認用(password_confimation)の入力フォームはmodelと直接関係のない属性です。こういった属性に対するロジックをmodelに書き続けた結果出来上がるのが、ファットモデルだと思っています。
controller側
フォームからの入力値をstrong parameterを通して受け取り、Formオブジェクトにデータを渡す
@form = UserRegistrationForm.new(user_registration_params)
@form.save
Formオブジェクト側
attr_accessor
として定義された属性に、controllerから渡されたハッシュデータをそれぞれセットすることで、変数のgetterとsetterを自動で生成している
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オブジェクトクラスで行っています。
他にも以下のようなユースケースが挙げられます。
- DB操作の対象にならないデータの処理
- ネストされた属性
- カスタムバリデーション
などです。
メリット
ここからはFormオブジェクトを用いるメリットについて説明しようと思います。
1. ビジネスロジックの隔離
Formオブジェクトを使用することで、コントローラからビジネスロジックを分離できます。フォームオブジェクトは、フォームデータのバリデーションやデータベースへの保存など、フォームに関連する操作をカプセル化するのでcontrollerがシンプルとなり、再利用性が上がります。
2. テスト容易性
当たり前ですが、Formオブジェクトは独立してテストできるため、テストケースを書くことが容易です。Formオブジェクトのメソッドに対してテストを実行し、フォームのバリデーションやデータ処理の正確性を確認できます。
3. フォームデータの整形
Formデータを整形して、DBモデルに適した形式に変換することができます。これにより、Formデータの取り込みとデータベースへの保存がスムーズに行えます。
今回の利用ケース
※ 実際の実装やテーブル名はぼかします
内容
「人物プロフィールを出身高校、出身大学、出身企業といった一貫性のあるデータを持つ」という実装
DB構成
なぜFormオブジェクトを用いるのか
今回の対象である出身高校、出身大学、出身企業の履歴を管理する各xxx_backgrounds
テーブルはカラムが全て対称である。
つまり、1経歴あたりUI上入力する内容は、**どの企業(高校ort大学)**か、いつからなのか、いつまでなのかであり、共通部分として考えられる。
MVCアーキテクチャを考えると、共通のFormから入力されたパラメータを捌くperson_backgrounds_controller.rb
を作成する必要があります。
ただ、このcontrollerには直接関係のあるDB(model)がありません
ここの共通の処理や入力パラメータ各modelに基づくDbとの連携の責務をFormオブジェクトとしてクラスに切り出すことにしました。
実装
以下に実装を示します
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
以下で上記実装について説明していく
-
PersonBackgroundFormクラスは、ActiveModel::Modelモジュールをインクルードしており、Active Recordモデルの代わりにデータのバリデーションと保存に使用できるFormオブジェクトを作成する
→ これにより、obj.create!
の際のvalidationを実行できる -
attr_accessorメソッドを使用して、このFormオブジェクトの属性を設定します。これらの属性はコード内で使用され、フォームから送信されたデータを格納する
-
initializeメソッドは、Formオブジェクトが作成されるときに属性を設定し、受け取ったハッシュ内の属性とその値をFormオブジェクトの属性に設定する
-
saveメソッドは、フォームから送信されたデータを受け取り、それをデータベースに保存する。このメソッド内で、person_typeの値に応じて異なるデータベーステーブルにデータを保存する
-
例外処理が行われており、データの保存が失敗した場合にエラーメッセージを返します。保存に失敗した場合、{ status: false, errors: ["error"] } の形式のハッシュを返す
また、このFormオブジェクトを以下のようにcontroller側で使用し、各parameterを渡す
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の責務を別クラスに切り出すことができる
本実装でのメリット
-
単一責任の原則
a. この実装では、フォームオブジェクトがビジネスロジックを担当しており、controllerやviewから分離されているので、Formオブジェクトはデータのバリデーションとデータベースへの保存に焦点を当て、単一の責任を持つ。 -
テスト容易性
b. Formオブジェクトは単体テストが容易である。フォームオブジェクトの各メソッドを独立してテストし、データのバリデーションや保存が期待どおりに動作することを確認することができる。
→ 従来通りの実装であれば、各3つのcontrollerに実装が分かれてしまうため、各controller specにテストコードを書く必要があるが、今回ビジネスロジックは全てFormオブジェクト内にカプセル化されているので、テスト用意性を担保できた -
可読性
c. ビジネスロジックがフォームオブジェクト内に集中しているため、controllerやviewのコードはシンプルで、可読性が向上する。
最後に
今回はRuby on Railsのデザインパターン、リファクタリング技法の一つである、Formオブジェクトでの実装を振り返った。
今まで何気なく書いていたコードでも共通部分をFormオブジェクトのクラスとしてカプセル化することでテスト見通しがよくなったり、亜k毒性の高いコードが書けるようになると感じた。
これからも取捨選択をしながら、この便利なパターンを採用して、可読性の高いコードを書けるように精進していきたい