中村的雑記

技術に関する記事を書いていきます。iOSエンジニア->Railsエンジニア。

FormObjectの実装

はじめに

業務でFormObjectを使って実装したので、サンプルコードを使って実装の要点をまとめる。
間違ってる点などあれば、指摘していただけると幸いだ。

状況

Userクラスのnameとemailと、Userクラスがhistoryクラスにdelegateしているorder_historyをUserControllerのupdateで更新したい。
ただ、何個の値が更新されるかわからない状況だ。(全て更新されるかもしれないし、一つしか更新されないかもしれない。)
(実際のプロジェクトでこんなモデルやクラスは作らなそうではあるが、今回あくまでサンプルなのでお目溢し願う)

実装

# frozen_string_literal: true
class User
  class UpdateForm
    include ActiveModel::Model
    include ActiveModel::Attributes

    attr_accessor :user

    attribute :name
    attribute :email
    attribute :order_history

    def initialize(user, attributes = {})
      @user = user
      default_attributes = {
        name: user.name,
        email: user.email,
        order_history: user.order_history,
      }
      super(default_attributes.merge(attributes))
    end

    def save!
      ActiveRecord::Base.transaction do
        user.update!(attributes.except("order_history"))
        user.history.update!(order_history: order_history)
      end
    end
        
   delegate :history, to: :user

  end
end

解説

include ActiveModel::Model

これによってvalidationやコールバックなどの機能が使えるようになる。

include ActiveModel::Attributes

これによってattributeメソッドが使えるようになる。
attributeを使うことでクラス属性の型を意識しなくても指定の型へ変換してくれている。
またこれを書くことで型を明示することもできる。

↓下記ページ参照させていただきました。 ushinji.hatenablog.com

attr_accessor :user

attr_accessorは読み取りも書き込みもできるオブジェクトの属性を定義したい時に使う。
ちなみに attr_reader は読み出し専用の属性を定義したいときに使い、
attr_writer は書き込み専用の属性を定義したいときに使う。

def initialize(user, attributes = {})
  @user = user
  default_attributes = {
    name: user.name,
    email: user.email,
    order_history: user.order_history,
  }
  super(default_attributes.merge(attributes))
end

このsuperはActiveModel::Attributesのinitializeメソッドをよんでいて、
これによってattributeメソッドが使えるようになる。
この時ActiveModel::Modelのinitializeメソッドは呼ばれていない。(はず)
なぜなら、Rubyでは同じ名前のメソッドを持つ複数モジュールをあるクラスでincludeし、
それらをsuperで呼び出した際、
後にincludeした方のモジュールのメソッドがそのクラスでは採用されるようになっているからだ。
例:

module Hello
  def greet
    puts 'Hello!!'
  end
end

module Ola
  def greet
    puts 'Ola!!'
  end
end

class Greeting
  include Hello
  include Ola


  def greet
    super
  end

end

g = Greeting.new
g.greet
# =>"Ola!!" 

なのでこのsuperはActiveModel::Attributesのinitializeメソッドだけを呼んでいる。

default_attributes = {
    name: user.name,
    email: user.email,
    order_history: user.order_history,
  }
super(default_attributes.merge(attributes))

ここでattributesを分けているのは、デフォルトの値を先に入れておいて、
mergeメソッドでコントローラーから渡されたattributesのhashをマージして、
変更箇所だけが変わったhashを作るため。

def save!
  ActiveRecord::Base.transaction do
    user.update!(attributes.except("order_history"))
    history.update!(order_history: order_history)
  end
end

updateする時は上記のように変更したいインスタンスをそれぞれアップデートする。 userをupdateする時は、exceptでorder_historyを除外したhashを引数に渡す。

以上。