RubyのコードローダーZeitwerk徹底解説:クラスとモジュールを自動管理する
RubyのコードローダーZeitwerk徹底解説:クラスとモジュールを自動管理する
Zeitwerkを使えば、クラスやモジュールがどこからでも利用可能になるため、プログラミングをよりシンプルかつ快適に進められます。
コードローダーとは?
コードローダーとは、開発者が複数のファイルやフォルダにまたがってclassやmoduleを定義しても、明示的なrequireなしにコードベース全体でそれらを利用できるようにする仕組みです。Railsはコードローダーを活用している代表例です。Railsでは、モデルをコントローラー内で使う前にいちいちrequireを書く必要がありません。実際、Rails 6では一部の例外を除き、appディレクトリ配下のコードはアプリ起動時に自動的に読み込まれます。
コードローディングは単にrequireを呼び出すだけと思われがちですが、実際にはもう少し複雑です。コードローディングは以下の3つの要素に分けて考えることができます。
- オートローディング(自動読み込み): 必要になった時点でコードをその場で読み込む方式です。例えばRailsでは、
rails sを実行してもすべてのモデルやコントローラーが読み込まれるわけではありません。Userモデルへの最初のアクセスが発生した時点で初めてオートロード機構が動作し、該当モデルを探して読み込みます。これがオートローディングの実例です。開発環境ではアプリやrails consoleの起動時間が短縮されるというメリットがあります。Rails.config.autoload_pathsで対象パスを制御できます。 - イーガーローディング(事前読み込み): アプリ起動時にすべてのコードをメモリへ読み込み、定数が参照されるのを待たない方式です。本番環境のRailsではこの方式が採用されています。本番でオートロードを使うと、定数ごとに都度読み込みが発生してレスポンスが遅くなってしまうためです。
Rails.config.eager_load_pathsで対象パスを制御できます。 - リローディング(再読み込み): コードローダーが
autoload_path内のファイル変更を常時監視し、変更を検知すると該当ファイルを再読み込みします。Railsの開発環境では、サーバーを再起動することなくrails sを実行したままコード修正ができるため非常に便利です。これがリローディングの実例です。
こうした概念の多くは、これまでRailsの中だけで発展してきました。しかし、Zeitwerkの登場によって状況は一変しました。Zeitwerkのおかげで、あらゆるRubyプロジェクトにコードローディングの仕組みを持ち込めるようになったのです。
Zeitwerkとは?
ZeitwerkはRuby向けの効率的かつスレッドセーフなコードローダーで、Webフレームワーク(Rails、Hanami、Sinatra)、CLIツール、gemなど、あらゆるRubyプロジェクトで利用できます。これを使えば、クラスやモジュールがどこからでも参照できることを前提に、すっきりとしたコーディングが可能になります。従来はRailsや一部のgemだけが独自のコードローダーを内蔵していましたが、Zeitwerkはその概念を独立したgemとして切り出し、すべてのRuby開発者が自分のプロジェクトに導入できるようにしました。
Zeitwerkのインストール
まずはgemをインストールしましょう。
gem install zeitwerk
# もしくはGemfileに以下を記述
gem 'zeitwerk', '~> 2.4.0'
Zeitwerkの設定
基本的な使い方から見ていきます。
require 'zeitwerk'
loader = Zeitwerk::Loader.new
...
loader.setup
上記のコードはloaderインスタンスを生成し、setupを呼び出しています。setupが呼ばれた時点で、loaderはコードを読み込める状態になります。ただしその前に、loaderに対して必要な設定はすべて済ませておく必要があります。ここでは主要な設定項目と、コードを整理するための規約をいくつか紹介します。
ファイル構造: Zeitwerkが正しく動作するには、ファイル名やディレクトリ名が定義先のモジュール名・クラス名と一致している必要があります。例えば以下のとおりです。
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
ルート名前空間: ルート名前空間とは、Zeitwerkがコードを探索する起点となるディレクトリのことです。モジュールやクラスが参照されると、Zeitwerkはルート名前空間内から対応するファイル名を探します。例えば以下のとおりです。
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("app/models")
loader.push_dir("app/controllers")
# 対応関係は以下の通り
# app/models/user.rb -> User
# app/controllers/admin/users_controller.rb -> Admin::UsersController
ルート名前空間の定義方法は主に2つあり、用途に応じて使い分けます。まずはデフォルトの方法です。
# init.rb
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/bar")
...
loader.setup
# bar/foo.rb
class Foo; end
この場合、barディレクトリがルート名前空間として機能するため、クラスFooはBar::Fooと書かずにFooとして直接参照できます。もう1つの方法は、push_dirの呼び出し時に名前空間を明示的に指定するやり方です。
# init.rb
require 'zeitwerk'
module Bar
end
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/src", namespace: Bar)
loader.setup
# src/foo.rb
class Bar::Foo; end
このコードで注目すべきポイントが3つあります。
- モジュール
Barは、push_dirで使用する前に定義済みである必要があります。使用したいモジュールがサードパーティ製の場合は、push_dirを呼び出す前に事前にrequireしておきましょう。 push_dirで名前空間Barを明示的に指定しています。- ファイル
src/foo.rbが定義しているのはBar::Fooであり、Fooではありません。そのため、src/bar/foo.rbのようなネストしたディレクトリ構造を作る必要はありません。
独立したコードローダー: 設計上、Zeitwerkはプロジェクトや依存gemごとに、それぞれのプロジェクトツリーを個別に管理できるようになっています。つまり各依存関係のコードロード機構は、その依存関係自身が担います。例えばRails 6では、Railsアプリ本体のコードロードをZeitwerkが担当しつつ、各gem依存関係はそれぞれ独自にプロジェクトツリーを管理できます。なお、複数のコードローダー間でファイルが重複するのはエラーとなるため注意が必要です。
オートローディング: 上記のセットアップを完了すれば、
setup呼び出し後はすべてのクラスとモジュールがオンデマンドで利用可能になります。リローディング: リローディングを有効化するには、loaderに対して明示的な設定が必要です。以下のように行います。
loader = Zeitwerk::Loader.new
...
loader.enable_reloading # setupの前にオプトインする必要がある
loader.setup
...
loader.reload
loader.reloadを呼び出すと、プロジェクトツリーが即座に再読み込みされ、新しい変更がすぐに反映されます。ただし、ファイルシステムの変更を検知してloader.reloadを呼び出す仕組みは別途用意する必要があります。簡単な実装例は次のとおりです。
require 'filewatcher'
loader = Zeitwerk::Loader.new
...
loader.enable_reloading
loader.setup
...
my_filewatcher = Filewatcher.new('lib/')
Thread.new(my_filewatcher) {|fw| fw.watch {|filename| loader.reload } }
RailsでZeitwerkを使う
Rails 6.0以降では、Zeitwerkがデフォルトで有効になっています。ただし、オプトアウトして従来のclassicコードローダーを使うことも可能です。
# config/application.rb
config.load_defaults "6.0"
config.autoloader = :classic
GemでZeitwerkを使う
Zeitwerkは、標準的なgem構成(lib/special_gem)に従っているgem向けに、便利なヘルパーメソッドを提供しています。このメソッドは次のように使えます。
# lib/special_gem.rb
require 'zeitwerk'
module SpecialGem
end
loader = Zeitwerk::Loader.for_gem
loader.setup
標準的なgem構成であれば、for_gemの呼び出しによってlibディレクトリがルート名前空間として登録され、lib配下のコードがすべて自動的に見つかるようになります。
さらに参考にしたい方は、Zeitwerkを採用している以下のgemを見てみると良いでしょう。
- Karafka
- Jets
参考文献
Rails autoloading — how it works, and when it doesn't
Zeitwerk
-
Rubyのdupとcloneの違いを徹底解説!オブジェクト複製の基礎知識
Rubyではオブジェクトをコピーできることをご存じでしょうか?しかも、コピー用のメソッドは2種類用意されています。 そのメソッドとは: dup clone それぞれの違いについては後ほど詳しく見ていきますが、まず最初に… そもそもなぜオブジェクトをクローンする必要があるのでしょうか? Rubyの多くのオブジェクトはミュータブル(変更可能)です。つまり、作成した後に内容を書き換えることができます。 元のオブジェクトはそのまま保持しつつ、内容を変更したい場合には、クローン(複製)を作成するのが有効です。 具体例を挙げてみましょう。 「先頭以外のすべての要素を含む配列」が欲しい場面を想定します。
-
Rubyでの静的分析入門!parser gemでメソッド定義を抽出する方法
ソースコードを解析して、すべてのメソッドがどこで定義され、どんな引数を受け取るのかを把握したいと思ったことはありませんか? どうすれば実現できるのでしょうか? 最初に思いつくのは、正規表現(regexp)を書くことかもしれません。 しかし、もっと良い方法があるとしたらどうでしょう? 答えは「あります」! 静的解析(Static Analysis)とは、ソースコードそのものから情報を抽出するためのテクニックです。 これは、ソースコードをトークンへと変換する(パースする)ことで実現されます。 それでは早速見ていきましょう! parser gemを使う Rubyには標準ライブラリとしてRipper