🦥

ActiveModel::Modelモジュールのinitializeメソッドは何をやっているか

2021/10/10に公開

はじめに

ActiveModel::Modelをincludeすると、
インスタンス生成時には単純にインスタンス変数への代入が行われているのではなくセッターメソッドを通して代入が行われていることがわかります。

# initializeメソッドを作成する場合
class User
  attr_reader :name

  def initialize(attributes = {})
    @name = attributes[:name]
  end

  def name=(name)
    @name = "#{name}君"
  end
end

User.new(name: "俺")
=> #<User:0x00007f8c42d26b28 @name="俺">
# ActiveModel::Modelをincludeする場合
class User
  include ActiveModel::Model
  attr_reader :name

  def name=(name)
    @name = "#{name}君で〜す"
  end
end

User.new(name: "俺")
=> #<User:0x00007f8c52d93150 @name="俺君で〜す">

そこで今回はActiveModel::Modelをincludeした時に呼び出されるinitializeメソッドを調査してみようと思います。

ActiveModel::Model

# rails/activemodel/lib/active_model/model.rb
module ActiveModel
  ..(省略)..
  module Model
		include ActiveModel::AttributeAssignment
		..(省略)..
    def initialize(attributes = {})
      assign_attributes(attributes) if attributes
      super()
    end
  end
end

rails/model.rb

initializeメソッドを見てみると、assign_attributesというメソッドが呼び出されています。このメソッドはActiveModel::AttributeAssignmentというモジュールに含まれています。

assign_attributes メソッド

# rails/activemodel/lib/active_model/attribute_assignment.rb
module ActiveModel
  module AttributeAssignment
    include ActiveModel::ForbiddenAttributesProtection

    def assign_attributes(new_attributes)
          # new_attributesがeach_pairメソッドを持たなければ(= hashでなければ)エラー
      unless new_attributes.respond_to?(:each_pair)
        raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
      end
      return if new_attributes.empty?

      # sanitize_for_mass_assignmentはマスアサインメントの際の許可に関するメソッドで、
            # インスタンス生成時にhashをそのまま入れる場合は関係ないので今回は省略します	
      _assign_attributes(sanitize_for_mass_assignment(new_attributes))
    end

    alias attributes= assign_attributes

    private
      def _assign_attributes(attributes)
        attributes.each do |k, v|
          _assign_attribute(k, v)
        end
      end

      def _assign_attribute(k, v)
        setter = :"#{k}="
        if respond_to?(setter)
          public_send(setter, v)
        else
          raise UnknownAttributeError.new(self, k.to_s)
        end
      end
  end
end

rails/attribute_assignment.rb

assign_attributesメソッドでは_assign_attributesというprivateメソッドが呼ばれていることがわかります。_assign_attributeメソッドではpublic_sendメソッドでセッターメソッドを呼んでいることがわかります。

# public_methodでメソッドを呼び出す例
class Child
  def greeting(name)
    p "おはようございます、#{name}さん!"
  end
end

Child.new.public_send("greeting", "太郎")
=> "おはようございます、太郎さん!"

これにより、ActiveModel::Modelをincludeするとinitializeメソッドではセッターメソッドが呼ばれていることがわかりました。

includeではなく、コードをそのまま持ってくると以下のようになります(エラー処理などは省略)

class Person
  attr_reader :name

	def initialize(attributes = {})
    assign_attributes(attributes) if attributes
  end

  def name=(name)
    @name = "#{name}君"
  end

  private

  def assign_attributes(attributes)
    _assign_attributes(attributes)
  end

  def _assign_attributes(attributes)
    attributes.each do |k, v|
      _assign_attribute(k, v)
    end
  end

  def _assign_attribute(k, v)
    setter = :"#{k}="
    if respond_to?(setter)
      public_send(setter, v)
    else
      raise UnknownAttributeError.new(self, k.to_s)
    end
  end
end

Person.new(name: "太郎")
=> #<Person:0x00007f8c52b22a60 @name="太郎君">

Discussion