AttributeSet::YAMLEncoderをModuleに変更してメモリ効率を改善
ActiveModel::AttributeSet::YAMLEncoderがClassからModuleに変更されました。この変更により、テーブルごとに生成されていたエンコーダーインスタンスが不要になり、メモリ効率が向上します。
背景
従来の実装では、各モデルクラスがdefault_typesを保持するYAMLEncoderインスタンスを@yaml_encoderとして保持していました。しかし、このdefault_typesはモデルクラスのattribute_typesとして既にキャッシュされており、YAMLEncoderで重複して保持する必要がありませんでした。
さらに、スキーマの再読み込み時にはreload_schema_from_cacheメソッド内で@yaml_encoderをクリアする処理が必要でした。これは、保持しているdefault_typesが古くなる可能性があるためです。この状態管理の複雑さがメンテナンス上の課題となっていました。
技術的な変更
YAMLEncoderの定義がClassからModuleに変更され、インスタンス化が不要になりました。
変更前:
class YAMLEncoder
def initialize(default_types)
@default_types = default_types
end
def encode(attribute_set, coder)
# ...
end
def decode(coder)
# ...
end
private
attr_reader :default_types
end
変更後:
module YAMLEncoder
extend self
def encode(attribute_set, coder, default_types)
# ...
end
def decode(coder, default_types)
# ...
end
end
メソッドシグネチャの変更により、default_typesがパラメータとして直接渡されるようになりました。呼び出し側では、モデルクラスのattribute_typesを直接渡します:
# encode時
ActiveModel::AttributeSet::YAMLEncoder.encode(@attributes, coder, self.class.attribute_types)
# decode時
ActiveModel::AttributeSet::YAMLEncoder.decode(coder, self.class.attribute_types)
YAMLEncoderインスタンスの管理が完全に削除されました。activerecord/lib/active_record/model_schema.rbから以下のコードが削除されています:
-
yaml_encoderメソッドの定義 -
@yaml_encoderインスタンス変数の初期化とキャッシング -
reload_schema_from_cacheでのクリア処理
これにより、スキーマ再読み込み時の状態管理が不要になりました。
設計判断
Moduleへの変更という設計が採用されました。PRの説明では、クラスとしての有用性がdefault_typesの保持のみであり、それがモデルクラスで既にキャッシュされている点が指摘されています。
Moduleにextend selfを適用することで、インスタンスメソッドをモジュール関数として使用できるようにしています。これにより、従来のencoder.encode(...)という呼び出しからYAMLEncoder.encode(..., default_types)という形式に変更されましたが、エンコーダーオブジェクトの生成とライフサイクル管理が不要になりました。
状態を持たない設計に変更したことで、各呼び出しで必要な情報をパラメータとして渡すため、複数のスレッドから同時に呼び出される場合の状態共有に関する懸念も解消されています。
まとめ
本PRは、YAMLEncoderの設計を「状態を持つクラス」から「状態を持たないモジュール」に変更することで、メモリ効率とコードの単純性を向上させました。テーブルごとのインスタンス生成が不要になり、スキーマ再読み込み時の状態管理も削減されています。必要な情報をパラメータとして渡す設計により、機能は維持したまま、より保守性の高い実装に改善されています。