🫠

Active Model の attribute はどのように書かれているのか? ②

に公開

4行目

https://github.com/rails/rails/blob/36601bbb02bc3570f5609db2127c77afca575d6c/activemodel/lib/active_model/attribute_registration.rb#L17

no_defaultという引数

まずは、no_defaultが気になります。
ここはこのdefの引数の時に、以下のようになっていました。
def attribute(name, type = nil, default: (no_default = true), **options)

知らなかった書き方です。()とかいけるんだ・・・

ちょっと検証してみました。
以下のようなmethodを定義します。

class Test
  # attr
  attr_accessor :name, :age

  def test_method(default: (no_default = true))
    puts "no_default: #{no_default}"
    puts "default: #{default}"
  end
end

パターン1:defaultを渡さないで実行

> test.test_method
no_default: true
default: true
=> nil

この時は、no_defaultがtrueになると。
no_defaultが引数のところで初期化できるのが驚きです。

ただ、この時にdefaultがtrueが入るのが不思議だし、注意しないと・・・・

パターン2:defaultを渡して実行

> test.test_method(default: "a")
no_default: 
default: a
=> nil

この時は、no_defaultは何も入らなくなりましたね。
nilでもないのが不思議です。defaultにaが入るのは想像通り。

ちょっとtest_methodの内容がよくなかったので、以下のように修正

def test_method(default: (no_default = true))
    puts "default: #{default}" if !no_default
    puts "no_default: #{no_default}" if no_default
  end

結果

> test.test_method
no_default: true
=> nil

> test.test_method(default: "a")
default: a
=> nil

この結果から見るに、no_defaultの真偽値を条件の判定に使用できることが確認できました。

PendingType???

さあ、戻って、そのno_defaultと、typeがある時(if type || no_default)に、PendingType.new(name, type)としていますね。

PendingTypeとか聞いたことないですね~
Pendingって、保留している?遅延実行的なことをしている?

いつもみたいに他のmoduleにあるのかなと思ったら、下の方で定義されていました。

https://github.com/rails/rails/blob/36601bbb02bc3570f5609db2127c77afca575d6c/activemodel/lib/active_model/attribute_registration.rb#L54-L59

こんな感じで変数を定義できたんですね!知らなかったです。defしか定義したことなかった。

まずは、Structクラスというみたこと無いクラスを調べます。
https://docs.ruby-lang.org/ja/latest/class/Struct.html
あああ、rubyの構造体クラスだったんですね・・・!
rubyで構造体を使ったことがない・・・とうか、すごい昔に学校でCを習った時にしか使ったことがないような。

さて、PendingTypeというStructを見てみます。

PendingType = Struct.new(:name, :type) do

まずは、:nameと:typeのメンバを定義していますね。また、doブロックを次で渡していそうです。

ブロックでは、次のようなインスタンスメソッドが定義されていますね。

def apply_to(attribute_set)
  attribute = attribute_set[name]
  attribute_set[name] = attribute.with_type(type || attribute.type)
end

attribute_setというParamsを期待していますね、また、次の行の

attribute = attribute_set[name]

からみるに、attribute_setは、何かしらのクラスなどだと予想。

このactivemodel/lib/active_model/attribute_registration.rbのrequireにそれっぽい定義がありました、

require "active_support/core_ext/class/subclasses"
>ココ require "active_model/attribute_set"
require "active_model/attribute/user_provided_default"

module ActiveModel

そのファイルに寄り道します・・・ 難しいのに読むとこ多くてツライ!!

https://github.com/rails/rails/blob/main/activemodel/lib/active_model/attribute_set/builder.rb

寄り道 class AttributeSet

さて、先ほどの名前からして、まずはAttributeSetクラスから見ていきましょう。

  class AttributeSet # :nodoc:
    class Builder # :nodoc:
      attr_reader :types, :default_attributes

      def initialize(types, default_attributes = {})
        @types = types
        @default_attributes = default_attributes
      end

      def build_from_database(values = {}, additional_types = {})
        LazyAttributeSet.new(values, types, additional_types, default_attributes)
      end
    end
  end

ええっと、まず普通になんでBuilderっていうクラスでラップしてるんだ・・・

この時のBuilderクラスのようなインナークラスは、親クラスの(今回はAttributeSet)の定数として扱われるみたいです。

また、名前空間の一部になるので、使うならスコープ解決演算子(::)を使用し、AttributeSet::Builderとする必要があると。

調べてみると、Builderパターンという記事がいくつかありました。
また、各種AIチャットにも聞きましたが、Builderパターンだと判定しました。

今の知識ではこのBuilderパターンのメリットがあんまり納得できないので、
まあ生成に一貫性を持たせれて、関するロジックを区切れていいな〜くらいで次に行きたいと思います。

このBuilderの内部の、肝心のbuild部分では、LazyAttributeSetをインスタンス化していますね。
ここら辺はコレを呼び出しているコードに会うまで置いておきます。

戻る

少し寄り道をしたので振り返ると、今は4行目を見ていました。

module ClassMethods # :nodoc:
      def attribute(name, type = nil, default: (no_default = true), **options)
        name = resolve_attribute_name(name)
        type = resolve_type_name(type, **options) if type.is_a?(Symbol)
        type = hook_attribute_type(name, type) if type

>       pending_attribute_modifications << PendingType.new(name, type) if type || no_default
        pending_attribute_modifications << PendingDefault.new(name, default) unless no_default

        reset_default_attributes
      end

~~~省略~~~

      private
        PendingType = Struct.new(:name, :type) do # :nodoc:
          def apply_to(attribute_set)
            attribute = attribute_set[name]
            attribute_set[name] = attribute.with_type(type || attribute.type)
          end
        end

そうですね、もしtypeがある時か、default値がないときはPendingType.new(name, type)をすると。

そしてその結果を、pending_attribute_modificationsに入れていますね。
コレあれですね、ここで入れとくだけで、PendingTypeが持つApply_toは行っていないので、遅延実行のための準備なんでしょうか・・・?

まだよくわかりません。

5行目

お次はこちらです。

pending_attribute_modifications << PendingDefault.new(name, default) unless no_default

先にPendingDefaultから見ると、以下の様になっています。

PendingDefault = Struct.new(:name, :default) do # :nodoc:
          def apply_to(attribute_set)
            attribute_set[name] = attribute_set[name].with_user_default(default)
          end
        end

メンバは、nameとdefaultを受け取り、
またPendingTypeのようにapply_toを持っていますね。

そして、unless no_defaultなのでデフォルト値があるときは、4行目と同じように
pending_attribute_modificationsに入れていますね。

4,5行目に関しては、これからどの様にしてpending_attribute_modificationsを呼び出したり使ったりしていくのか、気にしていきたいところです。

6行目

さて、やっとdef attributeの最後の行です。

reset_default_attributes

privateにありました。

        def reset_default_attributes
          reset_default_attributes!
          subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
        end

        def reset_default_attributes!
          @default_attributes = nil
          @attribute_types = nil
        end

面白いのが、
reset_default_attributesの中で、reset_default_attributes!を呼び出していますね。

reset_default_attributesの方は、さらに以下の処理をしています。

subclasses.each { |subclass| subclass.send(:reset_default_attributes) }

これ面白いです。まずはsubclassesっていうメソッドがあるのか?調べてみます。
おそらく、3行目でrequireしているこの中にありそうです。
なんだ、標準のObjectクラス全般で使えるのかと・・・

https://github.com/rails/rails/blob/36601bbb02bc3570f5609db2127c77afca575d6c/activemodel/lib/active_model/attribute_registration.rb#L3

ありました〜。これはactive support moduleなのかあ

https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/class/subclasses.rb

いや、ここでもsubclassesが使われていますね・・・

def descendantsという名前のものしかないですし_・・

これもrequireしてるファイルを見にいきます。

こちらでしょうか。

https://github.com/rails/rails/blob/58a2187f7b2ac391eba841e9ec6bb1a6d4e486dd/activesupport/lib/active_support/descendants_tracker.rb

コレはまた、読むのに一苦労しそうなファイルです・・・

ちょっと置いておきます。また別途調べてみます。


6行目に戻ると、
でも要は最後はattributeをnilでクリアしときましょうよ!ということですね!!

まとめ

今回はこちらのattributeクラスメソッドを見てみました。

普段の開発では書かないような記法であったり、クラス設計であったりするので、とても勉強になりました。

ただ、コレだけだとまだ謎な部分が多いですので、pending_attribute_modificationsの呼び出しについても追っていきたいなと思いました。(余力があれば。)

Discussion