Nested classes versus compact in Ruby

typeoneerror picture typeoneerror · Jun 24, 2014 · Viewed 7.9k times · Source

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.

Answer

Sami Samhuri picture Sami Samhuri · Jun 24, 2014

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.