ruby:如何正确要求(避免循环依赖)

use*_*335 14 ruby dependencies circular-dependency dependency-management

今天我遇到了一个奇怪的问题:模块上出现"遗漏方法"错误,但方法就在那里,并且需要定义模块的文件.经过一番搜索,我找到了一个循环依赖,其中2个文件需要彼此,现在我认为ruby默默地中止循环要求.


编辑开始:示例

文件'a.rb':

require './b.rb'

module A
    def self.do_something
        puts 'doing..'
    end
end
Run Code Online (Sandbox Code Playgroud)

文件'b.rb':

require './a.rb'

module B
    def self.calling
        ::A.do_something
    end
end

B.calling
Run Code Online (Sandbox Code Playgroud)

执行b.rb给出b.rb:5:in 'calling': uninitialized constant A (NameError).对于两个文件都必须存在这些要求,因为它们要从命令行自行运行(我省略了该代码以保持简短).所以B.calling必须在那里.一种可能的解决方案是将需求包装起来if __FILE__ == $0,但这似乎不是正确的方法.

编辑结束


避免这些难以发现的错误(顺便说一下,如果要求抛出异常会不会更好?),是否有一些关于如何构建项目的指导方针/规则以及在哪里需要什么?例如,如果我有

module MainModule
  module SubModule
    module SubSubModule
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

我应该在哪里需要子模块?所有在主要的,或只有主要的sub和sububub中的sub?

任何帮助都会很好.

摘要

在forforfs的回答和评论中讨论了为什么会发生这种情况的原因.

到目前为止,最佳实践(如lain指出或暗示)似乎如下(如果我错了,请纠正我):

  1. 将顶级命名空间中的每个模块或类放在以模块/类命名的文件中.在我的例子中,这将是一个名为'main_module.rb'的文件.如果有子模块或子类,则创建一个以模块/类命名的目录(在我的示例中为目录'main_module',并将子类/子模块的文件放在那里(在示例1中名为'sub_module.rb'的文件中)对命名空间的每个级别重复此操作.
  2. 需要一步一步(在示例中,MainModule将需要SubModule,Submodule并将需要SubSubModule)
  3. 将"运行"代码与"定义"代码分开.在运行代码中需要一次您的顶级模块/类,因此2.由于您的所有库功能现在都应该可用,并且您可以运行任何已定义的方法.

感谢所有回答/评论的人,这对我帮助很大!

iai*_*ain 10

在一段时间之后在Ruby邮件列表上询问这个问题后,当我以前在我的库中有一个文件只是为了要求时,我改为这两个规则:

  1. 如果一个文件需要来自同一个库中另一个的代码,我会require_relative在需要代码的文件中使用.

  2. 如果文件需要来自不同库的代码,我会require在需要代码的文件中使用.

据我所知,Ruby要求按顺序要求,因此循环依赖关系无关紧要.

(Ruby v1.9.2)

回答有关显示循环依赖问题的示例的注释:

实际上,示例的问题并不是需求是循环的,而是B.calling在需求完成之前调用.如果从b.rb中删除B.calling,它可以正常工作.例如,在代码文件中没有B.calling但之后运行的irb中:

$ irb
require'/ volume@RubyProjects/Test/stackoverflow8057625/b.rb'
=> true
B.calling
doing ..
=> nil

  • 此外,请注意(与`load`不同)对同一文件的多个`requires`只会导致文件的单个加载. (2认同)

for*_*orf 6

你希望已经知道几件基本的事情:

  1. Ruby是解释的,而不是编译的,所以你不能执行解释器没有看到的任何代码.

  2. require只需将文件中的代码插入到程序的那一点,换句话说,require程序顶部的a将require在底部之前解释.

(注意:编辑为require语句行为的帐户)所以如果你这样做: ruby a.rb这是ruby解释器将看到并执行的内容:

#load file b.rb <- from require './b.rb' in 'a.rb' file

#load file a.rb <- from require './a.rb' in 'b.rb' file
  #this runs because a.rb has not yet been required

#second attempt to load b.rb but is ignored <- from require './b.rb' in 'a.rb' file

#finish loading the rest of a.rb

module A
  def self.do_something
    puts 'doing..'
  end
end

#finish loading the rest of b.rb

module B
  def self.calling
    ::A.do_something
  end
end
B.calling

#Works because everything is defined 
Run Code Online (Sandbox Code Playgroud)

如果您首先运行b ruby b.rb,解释器会看到:

#load file a.rb <- from require './a.rb' in 'b.rb' file

#load file b.rb <- from require './b.rb' in 'a.rb' file
  #this runs because b.rb has not yet been required

#second attempt to load a.rb but is ignored <- from require './a.rb' in 'b.rb' file

#finish loading the rest of b.rb
module B
  def self.calling
    ::A.do_something
  end
end
B.calling #NameError, ::A.do_something hasn't been defined yet.
Run Code Online (Sandbox Code Playgroud)

希望这能解释其他人给你的好答案,如果你想一想,为什么很难回答你关于在哪里提出需求陈述的最后一个问题.使用Ruby,您需要的是文件而不是模块,因此将代码放在代码中的位置取决于文件的组织方式.

如果你绝对需要能够定义模块并且方法以随机顺序执行,那么你可以实现类似的东西来收集对尚不存在的模块的调用,然后在它们出现时调用它们.

module Delay
  @@q = {}  
  def self.call_mod(*args) #args format is method_name, mod_name, *args
    mod_name = args.shift
    method_name = args.shift
    #remaining args are still in args
    mod = Object.const_get(mod_name.to_sym)
    mod.send(method_name.to_sym, *args)
  end

  def self.exec(mod_name, *args)
    begin
      args.unshift(mod_name)
      self.call_mod(*args)
    rescue NameError, NoMethodError
      @@q[mod_name] ||= []
      @@q[mod_name] << args
    end
  end

  def self.included(mod)
    #get queued methods
    q_list = @@q[mod.name.to_sym]
    return unless q_list
    #execute delayed methods now that module exists
    q_list.each do |args|
      self.call_mod(*args)
    end
  end
end 
Run Code Online (Sandbox Code Playgroud)

一定要先定义延迟模块然后再调用B.calling你要使用的Delay.exec(:B, :calling, any_other_args).所以,如果你在延迟模块之后有这个:

Delay.exec(:B, :calling)   #Module B is not defined

module B
  def self.calling
    ::A.do_something
  end
  include Delay #must be *after* any callable method defs
end

module A
  def self.do_something
    puts 'doing..'
  end
  include Delay #must be *after* any callable method defs
end
Run Code Online (Sandbox Code Playgroud)

结果是:

#=> doing..
Run Code Online (Sandbox Code Playgroud)

最后一步是将代码分解为文件.一种方法可能是拥有三个文件

delay.rb   #holds just Delay module
a.rb       #holds the A module and any calls to other modules 
b.rb       #holds the B module and any calls to other modules
Run Code Online (Sandbox Code Playgroud)

只要你确定require 'delay'模块文件的第一行(a.rb和b.rb)和延迟包含在模块的末尾,事情应该有效.

最后注意:如果您无法将定义代码与模块执行调用分离,则此实现才有意义.