Form Object という選択肢を検討してみる

こんにちは。デザイン・システム室の岩田隼人です。
今日は私が実装する機会の増えた入力フォームに関連し、ただ実装するだけではなく
より汎用的で使いやすいフォームの実装を実現する方法について検討してみようと思います。

そもそも「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を綺麗に書くためのデザインパターンは他にもあるので、また機会があれば、ここでお伝えしていこうと思います。

ファンデリー デザイン・システム室ではこのように、システム全体の構成や設計を考えながら一緒に開発をする方を募集しております。興味を持ってくださった方はぜひ一度お話を聞きにきてください!

募集要項およびエントリーフォーム >