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

时间:2011-11-08 21:57:31

标签: ruby dependencies dependency-management circular-dependency

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


编辑开始:示例

档案'a.rb':

require './b.rb'

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

档案'b.rb':

require './a.rb'

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

B.calling

执行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

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

任何帮助都会非常好。

摘要

forforfs回答和评论中讨论了为什么会发生这种情况。

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

  1. 将顶级命名空间中的每个模块或类放在以模块/类命名的文件中。在我的例子中,这将是一个名为'main_module.rb'的文件。 如果有子模块或子类,则创建一个以模块/类命名的目录(在我的示例中为目录'main_module',并将子类/子模块的文件放在那里(在示例1中名为'sub_module.rb'的文件中) 。对你的命名空间的每个级别重复这个。
  2. 需要一步一步(在示例中,MainModule需要SubModule,而Submodule需要SubSubModule
  3. 将“运行”代码与“定义”代码分开。在运行代码中需要一次你的顶级模块/类,所以因为2.所有的库功能现在都应该可用,你可以运行任何已定义的方法。
  4. 感谢所有回答/评论的人,这对我帮助很大!

2 个答案:

答案 0 :(得分:10)

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

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

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

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

    (Ruby v1.9.2)

    回答有关显示循环依赖性问题的示例的评论:

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

      

    $ irb
      要求'/Volumes/RubyProjects/Test/stackoverflow8057625/b.rb'
      =>真正
      B.calling
      做..
      =>没有

答案 1 :(得分:6)

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

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

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

  3. (注意:编辑为需要声明行为的帐户) 所以,如果你这样做: require这就是ruby解释器会看到并执行的内容:

    ruby a.rb

    如果您首先运行b,#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 ,解释器会看到:

    ruby b.rb

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

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

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

    请务必先定义延迟模块,然后再调用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 来使用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
    

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

    #=> doing..
    

    只要您确保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 是模块文件的第一行(a.rb和b.rb)并且模块末尾包含Delay,那么事情就应该有效。

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