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

Posted

技术标签:

【中文标题】自动加载路径和嵌套服务类在 Ruby 中崩溃【英文标题】:Autoload paths and nested services classes crash in Ruby 【发布时间】:2019-01-05 02:36:01 【问题描述】:

我在 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 会在这里崩溃?

【问题讨论】:

请将您的一个服务定义(第一行应该这样做)添加到您的问题中。 你是什么意思?我应该写什么是对我的服务? 我的意思是,app/services/my_class/base.rb 是否像 class MyClass::Base 一样开头? 写在下面,抱歉我一开始没有高亮显示为代码,所以我不得不编辑它 哦,我明白了。这是不正确的。 class MyClass 应该是 module MyClass 【参考方案1】:

工作解决方案

经过更深入的调查和尝试,我意识到我必须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

【讨论】:

很高兴你能成功。至于您的问题:“我应该更改包装到命名空间中的类名,因为 Rails 告诉我这样做吗?”。 Rails 的口头禅之一是“约定优于配置”。所以,如果你相信这个口头禅,那么答案很可能是“是”。但是,你已经按照适合你的方式做事了。所以,干得好! 但是命名空间不应该用于此目的吗?拆分东西,这样你就可以处理它们而不用担心外面的东西?【参考方案2】:

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

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

然后useful_service中的任何独特方法:

app/services/sub_service/useful_service.rb

class SubService::UsefulService < SubService::ServiceBase

    def call
      a_subservice_method
      a_useful_service_method
    end

  private

    def a_useful_service_method
    end

end

然后,我可以这样做:

SubService::UsefulService.call(some: :args)

【讨论】:

好吧,但问题不在于这些,更多的是关于 rails 自动加载和自定义目录......最大的错误是 base.rb 显然不能在同一个项目中定义两次,并且具有相同模型名称的服务 - 可能发生 - 有某种不匹配.. Welp,我想关键是如果你像我展示的那样做(这与另一个答案几乎相同,IMO),那么自动加载和自定义目录将完美无缺。但是,是的,你是对的。如果您开始以非常规的方式做事,您将遇到自动加载和自定义目录的问题。 好吧,这不是非常规的,如果一个服务开始变得非常复杂(我说的是服务,但它可以是任何设计模式)你需要在类下面抽象成多个 PORO 所以它很容易测试,等等,但是为什么要对每个内部使用的子类使用“_service”以避免冲突?命名空间应该处理这个确切的问题。我希望你明白我的意思,我不明白 Rails 为什么这样做,以及如何正确解决它.. 仍在调查...... 在文件 'app/services/my_class/base.rb` 中定义class Base 内的class MyClass 非常规的,我相信。该文件结构通常认为MyClassmodule,而不是class。这两个答案都推荐。但是,嘿,我知道什么?顺便说一句,在我的项目中,我有几十个 PORO(服务、装饰器、演示者、管理器等),它们以无穷无尽的排列方式相互调用。对于我反复使用的东西,我将它们提取到宝石中。都是可测试的。都很可爱。 似乎这两个答案都没有为您提供令人满意的自动加载和自定义目录结构的解决方案。我会期待更好的答案。祝你好运。【参考方案3】:

用你的树,

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

【讨论】:

谢谢,我知道这一切,实际上我所有的服务类最后都添加了“_service.rb”,但是这些服务可能会变得非常复杂,所以我通常创建“my_service/”目录然后在里面......这就是它出错的地方。由于它可以包含的类数量,以及这些类在服务内部调用的事实,我保持名称简单,例如base.rb,但是当名称与某些模型或其他类匹配时,我开始得到奇怪的结果。 .. 现在我想知道如何找到一个好的解决方法 真正的问题是,如果是命名空间,为什么会出现名称崩溃的问题? 让我看看我能否真正快速地重现它,一秒钟。 我无法用一个非常幼稚的例子来重现它。如果您想在示例 Rails 应用程序中重现此内容并将其推送到 Github,我可以看看。 这是解决方案,人们。只需正确命名文件,并在代码中使用正确的模块和类名,它们就会自动加载,即使在子目录中也是如此。

以上是关于自动加载路径和嵌套服务类在 Ruby 中崩溃的主要内容,如果未能解决你的问题,请参考以下文章

自动加载常量时检测到循环依赖(Rails 4、Ruby 2)

在文件更改时自动重新编译和重新加载服务器

ruby 从命名空间#ruby #autoload自动加载一个类

嵌套的 Vue 子路由不会自动加载

ruby 根据`lib / tasks`目录中的目录结构自动在命名空间内加载rake任务。

在 Rails 中,如何在加载子对象时自动加载父对象?