Why you Should not use a Class as a Namespace in Rails Applications
This is a guest post by Junichi Ito (@jnchito). Junichi is a Ruby programmer at SonicGarden.jp, translator of Everyday Rails Testing with RSpec, and one of the most popular Ruby writers/bloggers in Japan
TL;DR
If you use a class as a namespace, it can produce a bug that doesn’t always show up on the surface. You should different names for your model class and your namespace in Rails applications.
class Staff < ApplicationRecord end # Shoud not use class as namespace class Staff::ItemsController < ApplicationController end # Should use a different name for namespace class Staffs::ItemsController < ApplicationController end
Hidden bug with a class name as a namespace
You can use both classes and modules as a namespace. But if you use a class, it might lead to unexpected behavior, especially in Rails applications.
For example, you have two classes called Item and Staff:
class Item < ApplicationRecord scope :published, -> { where(published: true) } end class Staff < ApplicationRecord end
You want to create two pages, one is for end users and the other is for staffs. End users can see published items only, but staffs can see all items. Then, you create these two controllers:
class ItemsController < ApplicationController def index @items = Item.published end end class Staff::ItemsController < ApplicationController def index @items = Item.all end end
But Staff::ItemsController
sometimes works wrong. It might return only published items. Why? This problem is related to Ruby’s constant lookup rule.
For an explanation, I’ll use a simpler example. If you run the code below in irb, you can refer to Staff::ItemsController
without definition:
class Item; end class Staff; end class ItemsController; end # Refer to Staff::ItemsController without definition Staff::ItemsController # warning: toplevel constant ItemsController referenced by Staff::ItemsController #=> ItemsController
You can see that Staff::ItemsController
returns ItemsController
. Here’s why:
- Top level constants always belong to an
Object
class, soItemsController
is equal toObject::ItemsController
. - When
Staff::ItemsController
is referred to, Ruby cannot find the constantItemsController
in theStaff
class. Then, Ruby tries to find it from theStaff
class’s ancestors. - You can confirm ancestor classes and modules with
Staff.ancestors
. It returns[Staff, Object, Kernel, BasicObject]
in the sample code above. - Ruby tries to find
ItemsController
in theObject
class which is next to theStaff
class in the ancestors array. Does theObject
class haveItemsController
? Yes, Ruby could findItemsController
successfully, becauseObject::ItemsController
is already defined. - But it might be a wrong lookup, so Ruby gives the warning “toplevel constant ItemsController referenced by Staff::ItemsController.” In fact, this case does not match our intention.
However, when Staff::ItemsController
is already defined, Staff::ItemsController
can be referred to not by ItemsController
, but by Staff::ItemsController
because Ruby can find ItemsController
in the Staff
class:
class Item; end class Staff; end class ItemsController; end class Staff::ItemsController; end # Refer Staff::ItemsController after definition Staff::ItemsController #=> Staff::ItemsController
When the application is run, sometimes Staff::ItemsController
can be referred to after definition and sometimes referred to before definition. The former leads to expected behavior and the latter leads to unexpected behavior. This is the hidden bug.
This problem can also occur in Rails applications. When you run Staff.ancestors
in the rails console, you can see a lot of classes and modules. But the Object
class does exist in ancestors. So this means that Staff::ItemsController
can refer to Object::ItemsController
when Staff::ItemsController
is not defined.
Use different names for your model class and your namespace
To avoid this problem, you should use different names for your model class and your namespace, for example, Staffs::ItemsController
.
If Staffs::ItemsController
is defined in the app/controllers/staffs
directory and the constant Staffs
is not defined yet, Rails defines the Staffs
module automatically. (This function is called “Automatic Modules”. Please see Rails Guides for details.)
When the namespace is a module, the constant lookup leads to different results from the class namespace, because the module does not have ancestors other than itself. Let us see the results in irb:
class Item; end class Staff; end class ItemsController; end module Staffs; end # Confirm Staffs module's ancestors Staffs.ancestors #=> [Staffs] # Refer to Staff::ItemsController without definition Staffs::ItemsController #=> NameError: uninitialized constant Staffs::ItemsController # Did you mean? ItemsController # from (irb):6 # from /Users/jit/.rbenv/versions/2.4.0/bin/irb:11:in `<main>'
This time, Staffs::ItemsController
was considered to be an uninitialized constant. This is because the Staffsmodule
does not have Object
in its ancestors. So Ruby won’t look up Object::ItemsController
and the constant lookup fails.
Rails can load Staffs::ItemsController without errors
In irb Staffs::ItemsController
is an uninitialized constant and Ruby produces an error, but this scenario is different in Rails applications. Rails implements the const_missing hook and tries to find Staffs::ItemsController
in autoload_paths
. If Staffs::ItemsController
is defined in autoload_paths like app/controllers/staffs/
, Rails can load Staffs::ItemsController
without any errors. Please see Autoloading and Reloading Constants in Rails Guides for details.
NOTE: The problem described in this post might be fixed in the future
The top-level constant lookup rule in Ruby might be changed in a future version, because the topic “remove top-level constant lookup” is being discussed here:
https://bugs.ruby-lang.org/issues/11547
Please check it out if you are interested.
By Junichi Ito.
—
Have a Ruby/Rails case to share with the community? Let us know in the comments below!
Your RubyMine Team