こんにちは、Web アプリケーションエンジニアのミツモトです。 普段は TUNAG という、企業やコミュニティを対象としたサービスの開発しています。 今回のブログでは、TUNAGのユーザー登録を実装するときに採用した、Rails の FormObject を取り上げます。
目次
はじめに
ユーザー登録にあたり、ユーザーだけでなく、その付属情報を扱う Model (所属など)も同時に保存する必要がありました。 それらを各々で処理すると、同じような処理を Controller で繰り返し書くことになり、見通しが悪くなります。 Rails の FormObject を採用することで、複数の Model を一緒に保存でき、 Controller の肥大化を防ぐことができました。 この記事では FormObject とその採用例をご紹介させていただきます。
FormObject
単一のフォーム送信で複数の ActiveRecord モデルを更新したい場合に、その永続化ロジックをカプセル化できるデザインパターンです。 ActiveModel::Model というモジュールを include することで利用できます。
メリット
- ビジネスロジックが Controller に出ないため、各レイヤーの可読性が良くなる
- 種類の異なる複数 Model を1つの Model として扱える
- validation がかけられる
- 渡ってきた parameter を FormObject のクラス内で parse できる
- DBに依存しないインスタンスでも、Active Recordと同じインターフェースで扱える(通知など)
採用例
採用する前
ユーザーのみを新規作成する場合、Controller は以下になります。
# ユーザーの新規作成フォームを表示 def new @user = User.new end # ユーザーを作成 def create @user = User.new(user_params) if @user.valid? @user.save end end
扱う Model が増えると...
# ユーザー、所属、住所の新規作成フォームを表示 def new @user = User.new @department = Department.new @address = Address.new . . . end # ユーザーを作成 def create @user = User.new(user_params) @department = Department.new(deparment_params) @address = Address.new(address_params) . . . if @user.valid? && @department .valid? && @address.valid? ... @user.save @department.save @address.save . . . end end
同じような処理が増えて、Controller が見辛くなります。
採用した後
User, Department, Address...をまとめるため、Form::Registration という FormObject のクラスを作ります。
Controller の処理
# 登録の新規作成フォーム def new @registration = Form::Registration.new end # 登録の実行 def create @registration = Form::Registration.new(registration_params) if @registration.valid? @registration.save # User と Department と Address が create される end end
Form::RegistrationのFormObjectクラス
class Form::Registration include ActiveModel::Model attr_accessor :user, :department, :address validate :validate_user validate :validate_department validate :validate_address def initialize(params: {}) @user = User.new(params[:user_params]) @department = Department.new(params[:department_params]) @address = Address.new(params[:address_params]) end def save @user.save @department.save @address.save end . . . end
まとめてvalidation をかけたり、parameter を FormObject のクラス内で parse できます。 また、各 Model の attributes ではないけど、保存するかどうかの判定に必要な parameter もここで受け取り、 Form::Registeration の validation として追加することができます。
このように FormObject を使うには、Form::Registration で include している ActiveModel::Model が必要です。
ActiveModel::Model
ActiveModel::Model を include することで、自分で定義したクラスを FormObject として Model のように扱うことができます。
中を見ると、
module ActiveModel module Model extend ActiveSupport::Concern include ActiveModel::AttributeAssignment include ActiveModel::Validations include ActiveModel::Conversion included do extend ActiveModel::Naming extend ActiveModel::Translation end . . . end end
ActiveModel 関連のモジュールが include , extend されています。
errors, valid? などのインスタンスメソッドがを扱える ActiveModel::Validations や、 エラーメッセージを翻訳できる ActiveModel::Translation が定義されています。
Model のようにインスタンスメソッドやクラスメソッドを呼ぶことができます。
おわりに
Rails の FormObject を取り上げました。 MVC アーキテクチャだけでは綺麗にコーディングできない部分を、別のレイヤーに切り出すことで、可読性を高めることができます。もし機会があれば、試しに使ってみてください。
最後まで読んでいただき、ありがとうございました。 スタメンではエンジニアを募集しています。興味がある方はぜひご連絡ください。