`cattr_accessor`/`mattr_accessor`からクラスインスタンス変数へ:Railsの内部実装整理
Railsの内部実装で広く使われていたcattr_accessor/mattr_accessorが、クラスインスタンス変数(singleton_class.attr_accessorまたはclass_attribute)へ段階的に置き換えられています。クラス変数の意味論的な問題を排除し、より予測可能な属性管理を実現する継続的なリファクタリングです。
背景
Rubyのクラス変数(@@var)は、継承ツリー全体で共有されるという独自の意味論を持ち、一般的なユースケースでは意図しない動作を引き起こしやすいことが知られています。cattr_accessor/mattr_accessorはこのクラス変数を内部で使用しており、サブクラスでの値の変更が親クラスや他のサブクラスに影響するという問題があります。
参照PR #42442 では、クラス変数のパフォーマンス上の問題も報告されています。同PRのベンチマークによると、クラス変数の読み取りはシングルトン属性と比較して、祖先クラスのない状態で約1.66倍、62の祖先クラスを持つActiveRecord::Baseのような場合にはさらに低速になることが示されています。クラス変数の探索は継承チェーンを遡る必要があるため、祖先の数に比例してコストが増加します。
本PRはその流れを継続するフォローアップとして、ActionMailbox、ActionPack、ActiveRecord、ActiveStorage、ActiveSupportの各コンポーネントにわたるcattr_accessor/mattr_accessorの使用箇所をまとめて整理しています。
技術的な変更
変更のパターンは対象属性の用途によって3種類に分類されます。それぞれ異なる置き換え戦略が採用されている点が特徴的です。
パターン1:class_attributeへの置き換え
サブクラスで上書き可能な属性にはclass_attributeが採用されています。actionmailbox/lib/action_mailbox/routing.rbのrouter、actionpack/lib/abstract_controller/callbacks.rbのraise_on_missing_callback_actions、actionpack/lib/action_controller/metal/strong_parameters.rbのpermit_all_parametersとaction_on_unpermitted_parameters、activesupport/lib/active_support/cache.rbのloggerとraise_on_invalid_cache_expiration_timeがこのパターンで置き換えられています。
変更前(actionpack/lib/action_controller/metal/strong_parameters.rb):
cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
変更後:
class_attribute :permit_all_parameters, instance_accessor: false, default: false
class_attribute :action_on_unpermitted_parameters, instance_accessor: false
パターン2:singleton_class.attr_accessorへの置き換え
モジュールレベルやクラスレベルで単純にクラスインスタンス変数として保持すればよい属性には、singleton_class.attr_accessorが使われています。activesupport/lib/active_support.rbのtest_order、test_parallelization_threshold、parallelize_test_databases、utc_to_local_returns_utc_offset_times、およびactivesupport/lib/active_support/json/decoding.rbのparse_json_timesがこのパターンです。
変更前(activesupport/lib/active_support.rb):
cattr_accessor :test_order
cattr_accessor :test_parallelization_threshold, default: 50
cattr_accessor :parallelize_test_databases, default: true
変更後:
singleton_class.attr_accessor :test_order
@test_parallelization_threshold = 50
singleton_class.attr_accessor :test_parallelization_threshold
@parallelize_test_databases = true
singleton_class.attr_accessor :parallelize_test_databases
デフォルト値はcattr_accessorのdefault:オプションに頼らず、インスタンス変数への直接代入で明示されるようになっています。
パターン3:class << self; attr_accessorへの置き換え
テストモデルクラスのような箇所では、class << selfブロック内のattr_accessorで置き換えられています。activerecord/test/models/developer.rbのinstance_countとactiverecord/test/models/membership.rbのcurrent_memberがこれにあたります。
# 変更前
cattr_accessor :instance_count
# 変更後
class << self
attr_accessor :instance_count
end
DateAndTime::Compatibilityモジュールの削除
注目すべき付随変更として、activesupport/lib/active_support/core_ext/date_and_time/compatibility.rbが完全に削除されています。このファイルはmattr_accessor :utc_to_local_returns_utc_offset_timesを提供するためだけに存在していましたが、この属性がActiveSupportモジュール自体のsingleton_class.attr_accessorに移動したことで不要になりました。DateTimeとTimeからもDateAndTime::Compatibilityのincludeが除去されています。
設計判断
class_attribute vs singleton_class.attr_accessorの使い分けが本PRの核心的な設計判断です。
class_attributeはActiveSupportが提供する機能で、サブクラスで独立して値を上書きできる継承対応の属性を定義します。ActionController::Baseのサブクラスがpermit_all_parametersを独自に変更しても他のコントローラに影響しない、という動作が期待される箇所に適用されています。一方、ActiveSupportモジュール自体の設定値や、継承ツリーを通じた共有が不要な箇所では、よりシンプルなsingleton_class.attr_accessorが選ばれています。
activesupport/lib/active_support/cache.rbではclass_attributeへの移行に伴い、テストコードのヘルパーもActiveSupport::Cache::Store.logger = ...というクラスレベルの一時変更から、@cache.with(logger: ...)というインスタンスレベルのスコープ付き変更に書き直されています。これにより、クラス変数のグローバルな状態変更を伴うテストのセットアップ・テアダウンが不要になり、テストの独立性が向上しています。
activesupport/lib/active_storage/blob.rbではscope_for_strict_loading内のstrict_loading_by_default?呼び出しにself.が明示的に付加されました。これはclass_attributeが生成するメソッドを確実にレシーバ付きで呼び出すための修正であり、変更の副産物として生まれた精緻化です。
まとめ
このPRは、クラス変数の意味論的な問題とパフォーマンス特性を踏まえ、用途に応じた適切な属性管理機構へ移行するRails内部のリファクタリングを前進させるものです。置き換えパターンを3種類に分類して適用することで、外部APIの互換性を維持しながら、内部実装をRubyの標準的なイディオムに近づけています。