Help us understand the problem. What is going on with this article?

Rails6から採用されたZeitwerkが最高だった

More than 1 year has passed since last update.

はじめに

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地獄 から救ってくれる救世主になりそうです :tada:

ちなみに、オートローダ変更の影響範囲がわからずに困っているお友達はいるかな!!?? :smiley::question:

それはきっとテストをちゃんと書いてないからだね? :smiley: :point_up:
テストを書く時間がないのではなく、テストを書かないから時間がなくなるんだぞっ (実体験) :v::smiley::v:

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away