Zeitwerkとrequire_dependency

こんにちは、ぽっけです。マネーフォワード クラウド会計Plusでエンジニアをしています。

Rails 6.0から、新しいAutoloaderとしてZeitwerkが導入されました。Rails 7.0からは旧来のAutoloader (Classicと呼びます)は使えなくなり、Zeitwerkが必須となりました。

Railsでソースコードを読み込むメソッドにrequire_dependencyがあります。require_dependencyは、Zeitwerkでは使う必要がなくなりました。 この記事では、require_dependencyがなぜZeitwerkで必要ないのかを深堀りしようと思います。

require_dependencyとは

そもそもrequire_dependencyとは何でしょうか? このメソッドは、Rubyデフォルトのrequireloadの代わりにRailsが提供しているメソッドです。

Autoloading と Reloading

まず、前提としてAutoloadingとReloadingについて簡単に説明します。

Railsは開発体験を向上させるため、定数のAutoloadingとReloadingを提供しています。

Autoloadingは、requireを必要なくする仕組みです。通常のRubyのプログラムではファイルを読み込むためにrequireメソッドを呼び出しますが、Railsではこれを使いません。その代わり、Railsでは定数名(クラス名)とファイル名を対応させ、定数へのアクセス時に自動でファイルを読み込むようにしています。

Reloadingは、定数を再読込する仕組みです。通常のRubyプログラムでは、一度読み込んだ定数はそのプロセスを再起動しない限り変化しません。つまり、ファイルを編集するたびにプロセスを再起動する必要があります。一方Railsではリクエストのたびに定数をすべて読み込み直す仕組みを提供しています。これによってbin/rails serverの再起動の必要なく開発を進めることができます。

Classic Loaderにおけるrequire_dependency

前述のAutoloadingによって、ほとんどのケースでは明示的なrequireは必要なくなりました。しかし、Classic Loaderではいくつかのケースにおいて明示的にファイルを読み込む必要がありました。

そのためにRailsはrequire_dependencyというメソッドを提供しています。このメソッドは通常のrequireloadを使ってファイルを読み込むとともに、読み込んだファイルが適切にAutoloaderとReloaderに管理されるようにします。Autoloaderの管理下にあるファイルを読み込む場合は、requireloadではなく、require_dependencyを使う必要がありました。

通常のケースでは定数はAutoloadingされるため、require_dependencyによる明示的な読み込みは不要です。ですが、いくつかのケースで明示的な読み込みが必要でした。 Classic LoaderのRails Guideではrequire_dependencyが必要なケースが具体的に2つ挙げられています。 https://railsguides.jp/autoloading_and_reloading_constants_classic_mode.html#require-dependency

Zeitwerkにおけるrequire_dependency

一方Zeitwerkではrequire_dependencyはobsoleteとなっています。 これはRails Guideや、require_dependencyメソッドのAPIドキュメントから確認できます。

Rails Guideの例

Zeitwerkによって、require_dependencyの既知のユースケースはすべて削除されました。プロジェクトをgrepしてrequire_dependencyをすべて削除してください。

https://railsguides.jp/classic_to_zeitwerk_howto.html#require-dependency呼び出しの削除

APIドキュメントの例

Warning: This method is obsolete. The semantics of the autoloader match Ruby’s and you do not need to be defensive with load order anymore. Just refer to classes and modules normally.

Engines that do not control the mode in which their parent application runs should call require_dependency where needed in case the runtime mode is :classic.

https://api.rubyonrails.org/classes/ActiveSupport/Dependencies/RequireDependency.html#method-i-require_dependency

このドキュメントの通り、require_dependencyが必要なのは、ZeitwerkとClassicのどちらが使われるかわからないRails Engineを開発している場合のみとなります。通常のRailsアプリケーションで使う必要はないでしょう。

どう置き換えたらよいのか

では、Zeitwerkではrequire_dependencyはどう置き換えていくと良いのでしょうか。Rails Guideで紹介されている方法を解説します。

STIを使っている場合

STIを使っている場合は、Rails Guideにあるコードをrequire_dependencyの代わりに使用すると良いでしょう。 https://railsguides.jp/autoloading_and_reloading_constants.html#sti(単一テーブル継承)

このコードを使用すると、DBからクラス名を読み取り、対応するクラスを自動的に読み込むようになります。これによって明示的なrequire_dependencyの必要なく、必要なクラスを読み込めます。

(個人的にはこのコードはworkaround感が強く、使うことに少々抵抗があります。とはいえ良い代替方法もないため、これを使うのが良いでしょう。StiPreload相当のものをActive Recordが提供してほしい気もする…。)

ちなみにSTIとAutoloadingの相性が悪い問題は、Classic Loaderのドキュメントで詳しく解説されています。 https://railsguides.jp/autoloading_and_reloading_constants_classic_mode.html#自動読み込みとsti Zeitwerkでも問題の原因は同じであるため、興味がある場合は読んでみると良いでしょう。

Classic Loaderで適切にAutoloadingできなかった定数の場合

これは、Rails Guideの次のページに説明されています。 https://railsguides.jp/constant_autoloading_and_reloading.html#定数がトリガーされない場合

これは異なる名前空間に同名のクラスがある時に問題となります。次の例を考えてみましょう。

class C
end
module N
  class C
  end
end
module N
  class D
    p C # => ???
   end
end

この例のn/d.rbで定数Cを参照するときの挙動が問題になります。Classic Loaderでは、c.rbn/c.rbの読み込み状況によって、n/d.rbで参照されるCが異なったものを指してしまっていました。そのため、Classic Loaderではn/d.rb内でCを評価する前に確実にn/c.rbを読み込む必要があり、そのためにrequire_dependencyが必要になっていました。

一方、Zeitwerkの場合はこのrequire_dependencyは全く必要ありません。ZeitwerkはRubyのautoloadメソッドを使って予めAutoloading対象の定数をすべて定義するため、読み込まれているファイルによる曖昧性が起きません。そのため、このようなケースでrequire_dependencyを使っていた場合には、これを単に削除できます。

その他の場合

この他の理由でrequire_dependencyを使っているケースもあるでしょう。その場合の方針は示されていませんが、単にrequire_dependencyを削除するか、もしくは定数を参照するコードでrequire_dependencyを置き換えると良いでしょう。

まず、require_dependencyで読み込みたい定数がコード中で参照されているような場合は、require_dependencyの呼び出しは削除できるでしょう。「Classic Loaderで適切にAutoloadingできなかった定数の場合」と同様の理由です。Rubyのautoloadが使われていることで定数解決の曖昧性が起こらず、Rubyの定数解決と全く同じようにAutoloadingがされるため、require_dependencyは必要ありません。

一方、STIのように定数が読み込まれていることに意味がある場合には、単にrequire_dependencyを削除するだけでは不十分です。たとえばClass#descentantsメソッドでクラスを列挙するようなケースが該当するでしょう。そのような場合は、require_dependencyの代わりに定数を参照することで定数を読み込むと良いでしょう。STIの例のようにDBなどから定数名を読み込んでも良いでしょうし、コード中に定数名を書いても良いでしょう。

どちらの場合でもrequire_dependencyrequireで置き換えてはいけません。Autoloading対象のファイルをrequireで読み込んではいけないことは、Rails Guideでも明記されています。 https://railsguides.jp/classic_to_zeitwerk_howto.html#不要なrequire呼び出しはすべて削除すること


いかがでしたでしょうか? この記事ではZeitwerkにおけるrequire_dependencyの立ち位置についてまとめました。基本的にはRails Guideなどにある内容の再構成ですが、require_dependencyに対処していく際にはまとまった情報として役立つかもしれません。

なお、Classic LoaderをZeitwerkに置き換えていく際にはrequire_dependency以外の対応も必要となるでしょう。下記でリンクしているRails Guideや、他社の事例を読むとその手助けになるでしょう。

⁠参考リンク

Rails Guideは、主に次の3つの記事が関連しています。

次のQiitaの記事には、Zeitwerkの挙動で問題となりやすい箇所がまとまっていて一読の価値があります。

require_dependencyにはあまり関係ありませんが、実際のアップデートの記録はZeitwerkへの切り替え作業時に役に立つでしょう。

今回解説した対象のコードは、次のリンクから主に見れます。


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【会社情報】
Wantedly
株式会社マネーフォワード
福岡開発拠点
関西開発拠点(大阪/京都)

【SNS】
マネーフォワード公式note
Twitter – 【公式】マネーフォワード
Twitter – Money Forward Developers
connpass – マネーフォワード
YouTube – Money Forward Developers

Pocket