Working on an initial Rails project, and using Rubocop to analyze code style. It led me to question exactly how Ruby's nested classes work in the context of Rails. For example, in my engine, I have a model:
# app/models/app_core/tenant.rb
module AppCore
class Tenant < ActiveRecord::Base
end
end
and a controller:
# app/controllers/app_core/tenant/members_controller.rb
module AppCore
class Tenant::MembersController < ApplicationController
end
end
In the model's case, the module is the same as the path and the class name is the same as the file name. In the controllers case, the second part of the path, "tenant" is part of the class name.
Rubocop tells me that I should "Use nested class definitions instead of compact style" in the Tenant::MembersController
line, so if I understand correctly...
module AppCore
class Tenant
class MembersController < ApplicationController
end
end
end
...this shouldn't make a difference.
Now, my question is I have AppCore::Tenant as a model, but then AppCore::Tenant looks to be reopened and the MembersController class is added to it as a nested class. Does this mean that my Tenant class will always have that nested class in it? Do I need to name my models and controller routes something differently? Is this totally fine and nothing to worry about? Not exactly sure what this means.
One subtle difference is that your scope is different, and this can cause errors. In the first case constants will be looked up in AppCore
, whereas in the second case constants will be looked up in AppCore::Tenant
. If you fully qualify constant names then it doesn't make a difference.
Foo = :problem
module A
Foo = 42
# looks up A::Foo because of lexical scope
module B
def self.foo
Foo
end
end
end
# looks up ::Foo because of lexical scope
module A::C
def self.foo
Foo
end
end
# Looks up A::Foo, fully qualified ... ok technically ::A::Foo is fully qualified, but meh.
module A::D
def self.foo
A::Foo
end
end
A::B.foo # => 42
A::C.foo # => :problem
A::D.foo # => 42
If you are referring to constants defined in AppCore::Tenant
from within MembersController
then it might make a difference for you. Subtle but possibly important, and good to be aware of. I've hit this in real life when I had a Util
module with a String
submodule. I moved a method into Util
and it broke because String
inside that method now referred to Util::String
. I changed some naming conventions after that.
Your Tenant
module will always have MembersController
as a nested class. Anywhere else in your codebase you can refer to AppCore::Tenant::MembersController
. If you want better separation then you should name your model classes differently, or put them inside a module such as AppCore::Model
or similar. If you're using Rails you'll have to buck some conventions, but the configuration required for that is not too bad.