Store の生成属性モジュールに名前を付けて匿名モジュールを排除
ActiveRecord::Store が生成する属性モジュールに固定名 GeneratedStoreMethods を付与し、匿名モジュールへの参照をインスタンス変数から定数名へ置き換えたことで、モデルの祖先表示が可読化され、余計なインスタンス変数の生成が抑止されます。
背景
従来、store_accessor が生成した属性メソッドは匿名モジュールに格納され、モデルの ancestors に #<Module:0x...> のような名前なしモジュールが現れていました。 そのためデバッグ時にどのモジュールが Store 用か判別しにくく、コードベースの把握コストが増大していました。
同時に、生成されたモジュールへの参照はシングルトンクラスのインスタンス変数 @_store_accessors_module に保持されていました。 サブクラスが継承されるたびにこの ivar が作成され、不要なクラス形状情報が蓄積される懸念がありました。
技術的な変更
store_accessor の実装を改変し、匿名モジュール生成ロジックを廃止して名前付きモジュール GeneratedStoreMethods を利用するようにしました。 まず定数が既に存在すれば取得し、無ければ Module.new を const_set で登録し、include しています。その後、取得したモジュールに対して module_eval でアクセサメソッドを定義します。
- _store_accessors_module.module_eval do
+ mod = if const_defined?(:GeneratedStoreMethods, false)
+ const_get(:GeneratedStoreMethods, false)
+ else
+ mod = const_set(:GeneratedStoreMethods, Module.new)
+ include mod
+ mod
+ end
+
+ mod.module_eval do
ClassMethods#inherited フックから @_store_accessors_module の初期化処理を削除し、@local_stored_attributes のみをリセットするようにしました。 さらに :nodoc コメントの表記も # :nodoc: へ統一されています。
- def inherited(subclass) # :nodoc
+ def inherited(subclass) # :nodoc:
super
subclass.instance_variable_set(:@local_stored_attributes, nil)
- subclass.instance_variable_set(:@_store_accessors_module, nil)
従来存在したプライベートメソッド _store_accessors_module を完全に削除しました。 これにより、外部からモジュールへアクセスする手段が一元化され、内部実装がシンプルになりました。
- def _store_accessors_module # :nodoc:
- @_store_accessors_module ||= begin
- mod = Module.new
- include mod
- mod
- end
- end
設計判断
匿名モジュールを排除し、名前付き定数 GeneratedStoreMethods を導入した判断は、可視性の向上と状態管理の簡素化です。 定数名でモジュールを特定できるため、ancestors に現れるモジュール名が明確になり、デバッグやメタプログラミング時の意図が把握しやすくなります。
インスタンス変数から定数参照へ切り替えることで、サブクラス継承時に余計な ivar が生成されないようになりました。 これによりクラス形状情報が統一され、内部オブジェクト構造が安定します。
互換性はそのまま保持されています。 既存コードが store_accessor を呼び出す限り、生成されるアクセサの振る舞いは変更されず、名前付きモジュールへの置き換えは内部実装にとどまります。
まとめ
本 PR は 匿名モジュールに固定名を付与し、インスタンス変数での参照を廃止 することで、ActiveRecord::Store の内部構造を可視化しつつクラス形状の安定化を実現しました。デバッグ性が向上し、余計な ivar が作られない設計へと改善されています。