Warm tip: This article is reproduced from stackoverflow.com, please click
ruby ruby-on-rails

Rails: It is strange to call module in module as Enum

发布于 2020-03-27 15:39:54

Sorry for my poor English.

I am using service layer in my Rails project.
Service classes are wrapped by module for namespace.
And a module has a module as Enum.
I try to call that enum module, but rails say StandardError exception: uninitialized constant normally.

app/helpers/application_helper.rb

def no_problem_method
  logger.debug LeaderBoard::FetchBoardMembersService.new.call
end

def problem_method
  logger.debug LeaderBoard::BoardType::TOTAL  # uninitialized constant LeaderBoard::BoardType
end

app/services/leader_board/fetch_board_members_service.rb

module LeaderBoard
  module BoardType
    TOTAL = 'total'
    IN_SESSION = 'in_session'
    GROUP = 'group'
  end.freeze

  class FetchBoardMembersService < BaseService

But, changing the calling order, it does not causing error.

app/helpers/application_helper.rb

def problem_method
  logger.debug LeaderBoard::BoardType::FetchBoardMembersService
  logger.debug LeaderBoard::BoardType::TOTAL  # SUCCESS!
end

Why this happen?
I have absolutely no idea. Please help... Further more, I put some code & logs for your understanding this strange things.

def problem_method
  logger.info LeaderBoard
  logger.info LeaderBoard.constants
  logger.info LeaderBoard::FetchBoardMembersService  
  logger.info LeaderBoard.constants
  logger.info LeaderBoard::BoardType
  logger.info LeaderBoard::BoardType::TOTAL
end

and logs

rails_1  | LeaderBoard
rails_1  | []
rails_1  | LeaderBoard::FetchBoardMembersService
rails_1  | [:BoardType, :FetchBoardMembersService]
rails_1  | LeaderBoard::BoardType
rails_1  | total
Questioner
mackeee
Viewed
87
max 2020-01-31 18:40

This is pretty simple really. The rails autoloader does not know that app/services/leader_board/fetch_board_members_service.rb declares the constantLeaderBoard::BoardType and how could it?

It has nothing to with with the fact that its an "Enum" which is not really a thing in Ruby. Thats just a module that declares a few constants and thus is no different from any other module, this is just a pattern that vaguely resembles enum types in other languages like C++ and Java.

When looking up a constant the autoloader makes assumptions based on the file location and expects the file to be located in one of the autoload paths. The authload paths in your average rails app is every subdirectory of /app. So when you reference LeaderBoard::BoardType Rails expects it to be defined in /app/**/leader_board/board_type.rb.

As you have already figured out it works if you have already referenced LeaderBoard::BoardType::FetchBoardMembersService since app/services/leader_board/fetch_board_members_service.rb has already been required.

The solution is also simple: lay your code out properly. The "root module file" should declare any constants that are not in seperate files.

# app/services/leader_board.rb
module LeaderBoard
  module BoardType
    TOTAL = 'total'.freeze
    IN_SESSION = 'in_session'.freeze
    GROUP = 'group'.freeze
  end.freeze
end
# app/services/leader_board/fetch_board_members_service.rb
module LeaderBoard
  class FetchBoardMembersService < BaseService
  end
end

This works since Rails will "walk up" the module nesting and autoload the "outer modules" first. Not only does this make it autoload properly - it also helps other developers find the code as this is the logical place to look if its not defined in its own file.