Railsのネストしたネームスペースのautoload
こんな感じのコードがあったとして
# app/models/user.rb class User end # app/models/admin/user.rb module Admin class User end end # app/controllers/admin/first_controller.rb class Admin::FirstController def index p User end end # app/controllers/admin/second_controller.rb module Admin class SecondController def index p User end end end
FirstControllerとSecondControllerの違いはモジュールのネストのシンタックスの違いだけ。このときRubyはUser定数の探索を次の順におこなう。
- FirstControllerの場合
- Admin::FirstController::User
- User
- SecondController
- Admin::FirstController::User
- Admin::User
- User
したがってFirstControllerはトップレベルのUser、SecondControllerはAdmin::Userを参照する。
$ ruby -r './app/models/user' -r './app/models/admin/user' -r './app/controllers/admin/first_controller' \ -e 'Admin::FirstController.new.index' User $ ruby -r './app/models/user' -r './app/models/admin/user' -r './app/controllers/admin/second_controller' \ -e 'Admin::SecondController.new.index' Admin::User
これがRubyの定数探索の挙動。
一方Railsのconst_missingによる探索の挙動は異なり、どちらの場合も以下の順で探索する。
- Admin::FirstController::User
- Admin::User
- User
$ rails r 'Admin::FirstController.new.index' Admin::User $ rails r 'Admin::SecondController.new.index' Admin::User
どちらもAdmin::Userを指す。Rails Guidesのドキュメントに以下のように書いてある。
Unfortunately Rails autoloading does not know the nesting in the spot where the constant was missing and so it is not able to act as Ruby would. In particular, Admin::User will get autoloaded in either case.
また、UserやAdmin::Userが事前に読み込まれているかどうかで、Rubyによる解決で済むかRailsのconst_missingの解決になるかが異なるので、場合によって挙動が違うようになる。これによりテストを実行したときに、単体のテストは通るけど全テストを実行したときに読み込み順によって落ちる、みたいなつらいことになりがち。
Userが読み込まれている場合はどちらの場合もRubyの探索でトップレベルのUserに行き着くのでUserを返す。これはわかりやすい。
$ rails r 'User; Admin::FirstController.new.index' User $ rails r 'User; Admin::SecondController.new.index' User
Admin::Userが読み込まれている場合はFirstControllerでエラーになる。Admin::Userを返してくれることを期待していたが、予想と異なる挙動になった。
$ rails r 'Admin::User; Admin::FirstController.new.index' uninitialized constant Admin::FirstController::User $ rails r 'Admin::User; Admin::SecondController.new.index' Admin::User
ドキュメントにはこのように書かれている。
If none of the parent namespaces of the class or module has the missing constant then Rails assumes the reference is relative. Otherwise qualified.
つまり、Admin::FirstControllerから未定義のUserを呼び出したとき、RailsはAdmin::FirstController::Userとして呼び出されたか、class Admin::FirstController; User; endとして呼び出されたかの判断ができない。したがって、このケースで、Admin::Userを返したら、Admin::FirstController::Userという呼び出しに対してAdmin::Userを返す可能性があってこれは非常にわかりづらい挙動になる。そのため、安全側に倒して親のネームスペースに探索対象の定数が定義されていたら親からは探さないようにする、ということらしい。
まとめると、思ったこととしてはこんな感じ。
- Railsでネストしたネームスペースの名前がかぶるような命名は基本的には避けるべき
- どうしてもネームスペースが重複する場合はその定数の参照はコンテキストに依存しない絶対的な参照に変更したほうがいい。
::User、Admin::Userのように class Foo::Bar; endのような表記はRubyの定数探索と挙動が異なるのでmodule Foo; class Bar; end; endのようなネストで書いたほうがよさそう気がするけど他にも想定してないケースがありそうなので本当にそれがいいかはあんまり自信ない
Rails難しい。