こんにちは。デザイン・システム室の岩田隼人です。
今日は私が実装する機会の増えた入力フォームに関連し、ただ実装するだけではなく
より汎用的で使いやすいフォームの実装を実現する方法について検討してみようと思います。
そもそも「Form Object」とは?
→Ruby on Railsのデザインパターン「設計手法」の1つです。
元々modelにバリデーション等様々な処理が書かれているものを、form層に切り出すことで多くのmodelで同一の処理を使えるなど、拡張性の高いコードを実現させることができるものです。 Railsで使用されているデザインパターンとしては他にもたくさんございますが、ここでは割愛します。
タイトルの内容を考えた背景
弊社サービス上で様々なお問い合わせ機能を実装をしていたのですが、その中での反省点が以下のようなcontrollerの処理を書いてしまっていたことです。
イメージ
class ContactController < ApplicationController def create con = Contact.new(contact_params) if params[:contact][:name].present? && params[:contact][:email].present? && params[:contact][:tel].blank? con.save end end private def contact_params params.require(:contact).permit(:name, :email, :tel) end end
この書き方の悪いところ
パラメータの数だけ分岐処理(if文)が追加されるので冗長なコードになってしまう。
→「fat controller」になってしまい、可読性が下がる。
修正その1
・controllerからバリデーション処理を切り出し、modelクラスに分ける
結果
class Contact < ActiveRecord::Base validates :name, :email, :tel, presence: true end class ContactController < ApplicationController def new @contact = Contact.new end def create @contact = Contact.new(contact_params) if @contact.valid? @contact.save end end private def contact_params params.require(:contact).permit(:name, :email, :tel) end end
この書き方の改善すべき点
保存するmodelの数だけ、controllerに同一の処理を記述しなければならないために冗長なコードになってしまう。
修正その2
・modelに書いてあった処理をform層に切り出し、他のmodelのバリデーションでも利用できる
ように対応する。
ここでは例として、住所用テーブル「Address」・顧客用テーブル「Customer」でも
バリデーションができるようにコードを書き換えてみます。
結果
class Form::Entry include ActiveModel::Model attr_accessor :customer, :contact, :address validate :validate_customer validate :validate_contact validate :validate_address def initialize(params: {}) @customer = Customer.new(params[:customer_params]) @contact = Contact.new(params[:contact_params]) @address = Address.new(params[:address_params]) end def save @customer.save @contact.save @address.save end end class Customer < ActiveRecord::Base validates :first_name, :last_name, presence: true end class Contact < ActiveRecord::Base validates :name, :email, :tel, presence: true end class Address < ActiveRecord::Base validates :prefecture, :city, presence: true end class ContactController < ApplicationController def new @entry = Form::Entry.new end def create @entry = Form::Entry.new(entry_params) if @entry.valid? @entry.save end end private def entry_params params.require(:entry).permit! end end
効果・メリット
・modelとformで入力値の検証かビジネスロジックの検証かを分別することができる。
・一度のフォーム送信時に複数の ActiveRecordモデルを更新しやすくできる。
・同じ処理をmodelに対して行う際にForm層の再利用が可能になる。
特にソースコードが何万行となってくると、機能改修の際に同じような処理をフォルダ検索等を使って探す可能性も生じますが、1つの場所(フォルダ)にまとめておけば、その必要も無くなります。
終わりに
今回はフォームの部分に焦点をあて、model、controller、formでのコードの書き方についてお伝えしました。
私は現在業務上既存コードを元にした開発が多い状態ですが、だからこそシステム上でより拡張性の高い・メンテナンス性の高いコードが書ける人になれるように、日々努力続けていこうと考えております。
既存のcontrollerやmodelを綺麗に書くためのデザインパターンは他にもあるので、また機会があれば、ここでお伝えしていこうと思います。
ファンデリー デザイン・システム室ではこのように、システム全体の構成や設計を考えながら一緒に開発をする方を募集しております。興味を持ってくださった方はぜひ一度お話を聞きにきてください!