`Date::DATE_FORMATS`/`Time::DATE_FORMATS` の非推奨化を改善し、依存関係を逆転させる
Date::DATE_FORMATS と Time::DATE_FORMATS の非推奨化メカニズムを改善し、フォーマット定義の実体をコア拡張モジュールから ActiveSupport::DateFormats / ActiveSupport::TimeFormats へと移管しました。これにより、モジュール間の依存関係が正しい方向に整理されています。
背景
このPRは、#57345 で導入された ActiveSupport::TimeFormats / ActiveSupport::DateFormats の実装に対するフォローアップです。前回の実装では、フォーマット定義の実体が Date / Time クラスのコア拡張側に置かれたまま、ActiveSupport::DateFormats / ActiveSupport::TimeFormats がそこから参照するという構造になっていました。この設計では、フォーマットモジュールがコア拡張に依存するという逆転した依存関係が生じていました。
また、非推奨警告についても、どのAPIに移行すべきかがユーザーに伝わりにくいという問題が指摘されていました。
技術的な変更
依存関係の方向を逆転させ、フォーマット定義の実体を ActiveSupport::DateFormats / ActiveSupport::TimeFormats 側に移しました。
変更前の構造:
active_support/date_formats.rb が active_support/core_ext/date/conversions を require し、Date::DATE_FORMATS の実体ハッシュを参照していました。
# 変更前
require "active_support/core_ext/date/conversions"
module ActiveSupport
module DateFormats
@list = Date::DATE_FORMATS.dup.freeze
@deprecated_list = Date::DATE_FORMATS
Date.deprecate_constant :DATE_FORMATS
end
end
変更後の構造:
フォーマット定義の実体が ActiveSupport::DateFormats / ActiveSupport::TimeFormats 側に移動し、コア拡張がフォーマットモジュールを require する形に逆転しました。
# 変更後
module ActiveSupport
module DateFormats
@list = {
short: "%d %b",
long: "%B %d, %Y",
db: "%Y-%m-%d",
inspect: "%Y-%m-%d",
number: "%Y%m%d",
long_ordinal: lambda { |date|
day_format = ActiveSupport::Inflector.ordinalize(date.day)
date.strftime("%B #{day_format}, %Y")
},
rfc822: "%d %b %Y",
rfc2822: "%d %b %Y",
iso8601: lambda { |date| date.iso8601 }
}.freeze
singleton_class.attr_reader :list
DEPRECATED_LIST = @list.dup
end
end
コア拡張側では、Date::DATE_FORMATS の実体ハッシュ定義を削除し、代わりに ActiveSupport::Deprecation::DeprecatedConstantAccessor を用いた非推奨定数宣言に置き換えました。
require "active_support/date_formats"
class Date
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
deprecate_constant :DATE_FORMATS, "ActiveSupport::DateFormats::DEPRECATED_LIST",
deprecator: ActiveSupport.deprecator,
message: "Date::DATE_FORMATS is deprecated, to register custom time formats use ActiveSupport::DateFormats.register"
end
Time 側も同様に、active_support/time_formats を require して Time::DATE_FORMATS を ActiveSupport::TimeFormats::DEPRECATED_LIST へのエイリアスとして非推奨化しています。
非推奨後の後方互換性については、DEPRECATED_LIST への書き込みが to_fs 時に参照されることをテストで確認しています。
def test_deprecated_to_fs_custom_date_format
assert_deprecated(ActiveSupport.deprecator) do
Date::DATE_FORMATS[:custom] = "%B %Y"
end
assert_equal "February 2005", Date.new(2005, 2, 21).to_fs(:custom)
ensure
ActiveSupport::DateFormats::DEPRECATED_LIST.delete(:custom)
end
設計判断
ActiveSupport.deprecator を使用した警告の統一と、移行先APIの明示という2点が今回の改善の核心です。
前回の実装では、Date.deprecate_constant :DATE_FORMATS を ActiveSupport::DateFormats モジュール内から呼び出していました。これは「フォーマットモジュールがコア拡張の初期化を担う」という設計上の不自然さを生んでいました。今回は ActiveSupport.deprecator を deprecate_constant の :deprecator オプションに明示的に渡すことで、Railsの標準的な非推奨メカニズムに準拠しています。
DEPRECATED_LIST の設計も注目に値します。@list.dup で作成した可変なハッシュを定数として公開することで、既存コードが Date::DATE_FORMATS[:custom] = ... のように書き込んだ場合も to_fs から参照されるという後方互換性が維持されます。lookup メソッドが @list[format] || DEPRECATED_LIST[format] の順で探索することで、新APIと旧APIの共存を実現しています。
まとめ
この変更は、依存関係の方向を「フォーマットモジュール→コア拡張」から「コア拡張→フォーマットモジュール」へと正しく逆転させつつ、ActiveSupport.deprecator による標準的な警告と移行先APIの明示を組み合わせた実装です。既存コードへの後方互換性を保ちながら、Date::DATE_FORMATS / Time::DATE_FORMATS から ActiveSupport::DateFormats.register / ActiveSupport::TimeFormats.register への移行路を明確に提示しています。