はじめに
Rails6がリリースされて早3ヶ月、
先週、大きめの案件が終わって暇になったので、社内のプロジェクトを全てRails5からRails6にアップデートしました。アップデート作業に伴い Zeitwerk について調べたところ、最高のGemだったのでまとめます。
Zeitwerkとは
Zeitwerk (読み方: ツァイトヴェルク) とは新しく開発されたオートロードの仕組みです。Gemとして独立しており、Railsに限らず様々なRubyプロジェクトで利用可能です。
なぜ開発されたのか?
Rails で 古くから採用されてきた const_missing
を利用したオートロードの仕組みには、処理順序によって発生する不具合など、エッジケースな落とし穴がいくつかありました。
Zeitwerkはそういったレガシーなオートロードの仕組みを改善する目的で開発が始まり、エッジケースの解消はもちろん、パフォーマンスに優れ、スレッドセーフで、汎用的に利用可能なオートローダーとしてリリースされました。
由緒正しき Kernel.#autoload
Zeitwerk はオートロードの仕組みを実現するために、Rubyに存在する Kernel.#autoload
を利用します。(このメソッド自体はRails4がリリースされた時期には存在しているようです。)
# 未読込のクラスを評価
irb> Hoge
=> NameError (uninitialized constant Hoge)
# オートロードされるモジュールを設定
irb> autoload :Hoge, '/libs/hoge'
=> nil
# 再度、未読込のクラスを評価
irb> Hoge
=> Hoge
Rails以外でZeitwerkを使う
普通のRubyアプリケーションの中で Zeitwerk を使ってみます。
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir('./libs')
loader.setup
Hoge.value = 'ほげほげ'
puts Hoge.value # => ほげほげ
たった4行追加するだけで、今まではRailsでしか使えなかったようなオートロードの仕組みを利用できます。
Ruby開発で悩みの種だった require地獄 からいとも簡単に抜け出せてしまうのです。これは最高の仕組みです。 (タイトルはココで回収)
Rails6とZeitwerk
ZeitwerkはRails6から正式なオートローダーとして採用されました。従来のオートローダーと挙動が異なる点があり、切り替える際には気をつけなければならない事項がいくつか存在します。
初期化時のオートローダ利用が非推奨になった
初期化時にオートローダーでモジュールを読み込むことが非推奨となりました。
config/initializers/*.rb
等からモジュールをオートロードしようとすると DEPRECATION WARNING: Initialization autoloaded the constants XXXXXX
と言う警告が表示されます。
これは 明示的にモジュールをrequire
することで解消可能ですが、なぜこれで解消されるのでしょうか?
この警告を無視すると何が起きるのか?
この警告を無視した場合、何が起きるのか検証してみました。
# 検証用モジュール (app/libs/hoge.rb)
class Hoge
class << self
attr_accessor :value
end
end
# コントローラ (app/controllers/appliction_controller.rb)
class ApplicationController < ActionController::Base
before_action do
puts "Hoge.value = #{Hoge.value.inspect}"
end
end
# イニシャライザ (config/initializers/hoge.rb)
Hoge.value = 'ほげ'
コントローラが実行された際に出力される文字列は期待に反して Hoge.value = nil
です。
警告が出ないようにイニシャライザ (config/initializers/hoge.rb) で明示的に require
してみます。
require 'hoge'
Hoge.value = 'ほげ'
今度は期待通り Hoge.value = "ほげ"
が出力されました。
何が起こっているのか?
Zeitwerkのログを出力して観察してみます。
# config/application.rb
Rails.autoloaders.logger = ActiveSupport::Logger.new(STDOUT)
明示的に require
しない場合のログです。
Zeitwerk@rails.main: autoload for Hoge removed
Zeitwerk@rails.main: autoload set for Hoge, to be loaded from /app/libs/hoge.rb
読み込んだモジュールが削除されるログが出力されています。(Rails が reload!
等を呼び出すタイミングでしょうか?)
次は明示的に require
した場合のログです。
Zeitwerk@rails.main: file /app/libs/hoge.rb is ignored because Hoge is already defined
読み込み済みモジュールはZeitwerkによる管理が無効になるようです。
この挙動により、明示的に require
した場合は期待通りの動きになります。
名前推測の挙動が変わった
従来のオートローダーは、定数名からファイル名を推測しました。(String#underscore
)
しかし、Zeitwerkではファイル名から定数名を推測します。(String#camelize
)
# HogeAPI から hoge_api.rb をロード可能
irb> "HogeAPI".underscore
=> "hoge_api"
# hoge_api.rb から HogeAPI はロード不可能
irb> "hoge_api".camelize
=> "HogeApi"
このようなケースでは、従来のオートローダーをZeitwerkに切り替えるとアプリケーションは動作しなくなります。
その場合、Inflectorの設定によりこの問題を解決可能です。
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'API'
end
もし、アプリケーション全体に影響与えたくない場合はオートローダーの設定で問題を解決可能です。
# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
autoloader.inflector.inflect(
"hoge_api" => "HogeAPI"
)
end
クラシックモード
Rails6でもオートローダーにクラシックモードを指定することで従来のオートローダーを利用できます。
# config/application.rb
config.load_defaults '6.0'
config.autoloader = :classic
ただし、いつ削除されるかわからないので早めに移行しましょう!
最後に
Zeitwerk は Rubyの汎用的なプロジェクトで悩みの種だった require地獄 から救ってくれる救世主になりそうです
ちなみに、オートローダ変更の影響範囲がわからずに困っているお友達はいるかな!!??
それはきっとテストをちゃんと書いてないからだね?
テストを書く時間がないのではなく、テストを書かないから時間がなくなるんだぞっ (実体験)