Подтвердить что ты не робот

Ruby: как правильно (чтобы избежать круговых зависимостей)

Сегодня у меня возникла странная проблема: получил ошибку "недостающий метод" в модуле, но метод был там, и был необходим файл, в котором был определен модуль. После некоторого поиска я нашел круговую зависимость, где 2 файла требовали друг друга, и теперь я предполагаю, что рубиновый молчание прерывает круговое обращение.


Изменить начало: Пример

Файл '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" ) повторите это для каждого уровня вашего пространства имен.
  • требуется шаг за шагом (в примере для MainModule потребуется SubModule, а для SubModule потребуется SubSubModule)
  • отделить "код" от кода "define". в исполняемом коде требуется один раз ваш модуль/класс верхнего уровня, поэтому из-за 2. теперь все функции библиотеки должны быть доступны, и вы можете запускать любые определенные методы.

спасибо всем, кто ответил/прокомментировал, это мне очень помогло!

4b9b3361

Ответ 1

После запроса об этом в списке рассылки Ruby некоторое время назад, когда я использовал файл в своих библиотеках только для того, чтобы требовать вещи, я изменил эти два правила:

  • Если файл нуждается в коде из другого в той же библиотеке, я использую require_relative в файле, который нуждается в коде.

  • Если файлу нужен код из другой библиотеки, я использую require в файле, который нуждается в коде.

Насколько я понимаю, Ruby требуется в том порядке, в котором он задан, и поэтому не имеет значения, что касается круговых зависимостей.

(Ruby v1.9.2)

В ответ на комментарий о примере, показывающем проблемы с круговой зависимостью:

на самом деле проблема с примером заключается не в том, что требования являются циклическими, но что B.calling вызывается до завершения запроса. Если вы удалите B.calling из b.rb, он отлично работает. Например, в irb без B.calling в файле кода, но потом запускайте:

$irb
require '/Volumes/RubyProjects/Test/stackoverflow8057625/b.rb'
= > true
B.calling
делать..
= > nil

Ответ 2

Несколько основных вещей, которые вы, надеюсь, уже знаете:

  • Ruby интерпретируется, а не компилируется, поэтому вы не можете выполнять код, который не был обнаружен интерпретатором.

  • require просто вставляет код из файла в эту точку программы, другими словами, 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 

Если вместо этого вы выполнили 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.

Надеюсь, это объяснит хорошие ответы, которые вам дали другие, и если вы думаете об этом, почему это трудно ответить на ваш последний вопрос о том, где поставить заявления. С 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 

Обязательно сначала определите модуль Delay, а затем вместо вызова B.calling используйте Delay.exec(:B, :calling, any_other_args). Поэтому, если у вас есть это после модуля Delay:

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

Пока вы убедитесь, что require 'delay' - это первая строка файлов модуля (a.rb и b.rb) и Delay, включенная в конце модуля, все должно работать.

Заключительное примечание. Эта реализация имеет смысл только в том случае, если вы не можете отделить код определения от вызовов выполнения модуля.