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
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.