自动加载路径和嵌套服务类在Ruby中崩溃

时间:2018-07-28 00:10:33

标签: ruby-on-rails ruby namespaces ruby-on-rails-5

我在Rails 5项目的app/services文件夹下需要加载/需要类的问题很多,而我开始放弃这个问题。

首先要明确的是,services/是我在整个项目中使用的简单PORO类,用于从控制器,模型等中抽象出大多数业务逻辑。

树看起来像这样

app/
 services/
  my_service/
    base.rb
    funny_name.rb
  my_service.rb  
models/
 funny_name.rb

失败#1

首先,当我尝试使用MyService.const_get('FunnyName')时,它从我的模型目录中获得了FunnyName。但是,当我直接进行MyService::FunnyName时,它似乎没有相同的行为,但是在我的大多数测试和更改中,这都可以正常工作,这很奇怪。

我意识到Rails config.autoload_paths不会递归加载;可以理解的是,第一个FunnyNamemodels/funny_name.rb,因为它肯定是已加载的,而不是其他。

没关系,让我们找到一种解决方法。我将此添加到了application.rb中:

config.autoload_paths += Dir[Rails.root.join('app', 'services', '**/')]

这会将服务的所有子目录添加到config.autoload_paths中。显然,从Rails 5开始,不建议编写类似的内容。但是这个主意确实适合我。

失败#2

现在,当我启动我的应用程序时,它崩溃并输出如下内容

  

无法自动加载常量Base,应为   /.../backend/app/services/my_service/base.rb进行定义(LoadError)

名称已更改,但这是我之前写的树的匹配路径

问题是,base.rb是在错误导致我进入的确切文件中定义的,其中包含类似

的内容
class MyService
  class Base
  end
end

解决方案不佳

因此,我尝试了其他解决方法,其中很多方法都无效。因此,我最终完全删除了autoload_paths并将其直接添加到application.rb

Dir[Rails.root.join('app', 'services', '**', '*.rb')].each { |file| require file }

现在base.rb已正确加载,MyService.const_get('FunnyName')实际上将返回正确的类,并且一切正常,但这是一种令人作呕的解决方法。另外,它尚未在production中进行过测试,但根据环境的不同,可能会产生问题。

application.rb中请求整棵树听起来是个坏主意,我认为不能这样保存。

在Rails中添加自定义services/目录的最干净方法是什么?它包含多个具有简单名称的子目录和类,这些名称也存在于应用程序的其他部分(模型,base.rb等)

如何避免混淆autoload_paths?还有其他我不知道的方法可以解决这个问题吗?为什么base.rb甚至在这里崩溃?

3 个答案:

答案 0 :(得分:3)

工作解决方案

经过更深入的研究和尝试,我意识到我必须eager_load这些服务以避免在调用诸如const_get('MyClassWithModelName')之类的元功能时获取错误的常量。

这就是问题所在:经典的eager_load_paths无法正常工作,因为出于某种原因,这些类显然会在初始化Rails的整个核心之前就被加载,而简单的类名如Base实际上会与内核混合在一起,因此会使所有崩溃。

有人会说“然后将Base重命名为其他名称”,但是我应该更改包装到名称空间中的类名称,因为Rails告诉我这样做吗?我不这么认为。 类名应该保持简单,我在自定义名称空间中所做的事与Rails无关。

我必须仔细考虑一下,并写下自己的Rails配置钩子。我们加载内核及其所有功能,然后递归service/

附带说明,它不会增加生产环境的重量,并且非常便于开发。

要添加的代码

将其放置在config/environment/development.rb以及您希望加载而不会出现Rails类冲突的所有其他环境中(例如test.rb

# we eager load all services and subdirectories after Rails itself has been initializer
# why not use `eager_load_paths` or `autoload_paths` ? it makes conflict with the Rails core classes
# here we do eager them the same way but afterwards so it never crashes or has conflicts.
# see `initializers/after_eager_load_paths.rb` for more details
config.after_eager_load_paths = Dir[Rails.root.join('app', 'services', '**/')]

然后创建一个包含该文件的新文件initializers/after_eager_load_paths.rb

# this is a customized eager load system
# after Rails has been initialized and if the `after_eager_load_paths` contains something
# we will go through the directories recursively and eager load all ruby files
# this is to avoid constant mismatch on startup with `autoload_paths` or `eager_load_paths`
# it also prevent any autoload failure dû to deep recursive folders with subclasses
# which have similar name to top level constants.
Rails.application.configure do
  if config.respond_to?(:after_eager_load_paths) && config.after_eager_load_paths.instance_of?(Array)
    config.after_initialize do
      config.after_eager_load_paths.each do |path|
        Dir["#{path}/*.rb"].each { |file| require file }
      end
    end
  end
end

像魅力一样工作。如果需要,您还可以通过require来更改load

答案 1 :(得分:1)

当我这样做(在我所有的项目中)时,它看起来像这样:

$(document).ready(function() {
		$("table #libelleArticle").change(function() { 
			// le problem how to send a price param from thymeleaf to jquery  
			$("#prix").val(" "); // price input
		});
});

我将所有常见的方法定义放在app |- services | |- sub_service | | |- service_base.rb | | |- useful_service.rb | |- service_base.rb 中:

app / services / service_base.rb

app/services/service_base.rb

我在class ServiceBase attr_accessor *%w( args ).freeze class < self def call(args={}) new(args).call end end def initialize(args) @args = args end end 中放置了sub_services通用的所有方法:

app / services / sub_service / service_base.rb

app/services/sub_service/service_base.rb

然后是class SubService::ServiceBase < ServiceBase def call end private def a_subservice_method end end 中的所有唯一方法:

app / services / sub_service / useful_service.rb

useful_service

然后,我可以做类似的事情:

class SubService::UsefulService < SubService::ServiceBase

    def call
      a_subservice_method
      a_useful_service_method
    end

  private

    def a_useful_service_method
    end

end

答案 2 :(得分:0)

带着你的树

app/
 services/
  my_class/
    base.rb
    funny_name.rb
  my_class.rb  
models/
 funny_name.rb

services / my_class / base.rb应该类似于:

module MyClass
  class Base

services / my_class / funny_name.rb应该类似于:

module MyClass
  class FunnyName

services / my_class.rb应该类似于:

class MyClass

models / funny_name.rb应该类似于:

class FunnyName

我说“应该看起来像”,因为类/模块是可互换的; Rails只是想在这些位置定义这些常量。

您无需在自动加载路径中添加任何内容。 Rails会自动提取app

中的所有内容

轶事:在您的服务目录中,将它们的命名约定(文件名和基础常量)视为“ _service.rb”或“ ThingService”是很常见的,就像控制器的外观一样。模型没有这个后缀,因为它们被视为一流的对象。

GitLab具有一些很棒的文件结构,非常值得一看。 https://gitlab.com/gitlab-org/gitlab-ce