ruby: как правильно требовать (чтобы избежать циклических зависимостей) - PullRequest
14 голосов
/ 09 ноября 2011

сегодня я столкнулся со странной проблемой: получил ошибку «отсутствует метод» на модуле, но метод был там, и был необходим файл, в котором был определен модуль.После некоторого поиска я нашел циклическую зависимость, где 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).Требование должно быть там для обоих файлов, так как они предназначены для запуска самостоятельно из командной строки (я пропустил этот код, чтобы сохранить его кратким).Так что Б. должен быть там.Одним из возможных решений является включение требований в if __FILE__ == $0, но это не совсем правильный путь.

Редактировать конец


, чтобы избежать этих труднодоступных ошибок(кстати, не было бы лучше, если бы требование вызвало исключение?), есть ли какие-то руководящие принципы / правила о том, как структурировать проект и где что требовать?Например, если у меня есть

module MainModule
  module SubModule
    module SubSubModule
    end
  end
end

, где мне требуются субмодули?все в основном, или только саб в основном и суб в суб?

любая помощь будет очень хорошей.

Резюме

Объяснение, почему это происходитобсуждается в ответе и комментариях forforfs.

До сих пор наилучшая практика (на которую указывает или намекнул лэйн) выглядит следующим образом (пожалуйста, исправьте меня, если я ошибаюсь):

  1. помещает каждый модуль или класс в верхнее пространство имен в файле с именем модуля / класса.в моем примере это будет 1 файл с именем 'main_module.rb.'если есть подмодули или подклассы, создайте каталог, названный в честь модуля / класса (в моем примере это каталог 'main_module', и поместите туда файлы для подклассов / подмодулей (в примере 1 файл с именем 'sub_module.rb')Повторите это для каждого уровня вашего пространства имен.
  2. требуется пошагово (в этом примере MainModule потребует SubModule, а Submodule потребует SubSubModule)
  3. отделяет «работающий» код от «определяющего» кода. В работающем коде требуется один раз ваш модуль / класс верхнего уровня, поэтому из-за 2. теперь все функции вашей библиотеки должны быть доступны, и вы можете запуститьопределенные методы.

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

Ответы [ 2 ]

10 голосов
/ 09 ноября 2011

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

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

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

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

(Ruby v1.9.2)

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

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

$ irb
require '/Volumes/RubyProjects/Test/stackoverflow8057625/b.rb'
=> верно
B.calling
делает ..
=> ноль

6 голосов
/ 10 ноября 2011

Пара основных вещей, о которых вы, наверное, уже знаете:

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

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

Обязательносначала определите модуль задержки, а затем вместо вызова 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

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

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

...