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難しい。