Community

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, so ItemsController is equal to Object::ItemsController.
  • When Staff::ItemsController is referred to, Ruby cannot find the constant ItemsController in the Staff class. Then, Ruby tries to find it from the Staff 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 the Object class which is next to the Staff class in the ancestors array. Does the Object class have ItemsController? Yes, Ruby could find ItemsController successfully, because Object::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

image description