在 Rails 中运行 rake 任务时的方法命名空间冲突



Using Rails 2.3.10如果我的库/任务看起来像这样

lib/tasks
- a.rake
- b.rake

a.rake看起来像这样:

namespace :a do
    desc "Task A"
    task(:a=>:environment)do
      msg('I AM TASK A')
    end
    def msg(msg)
      puts "MSG SENT FROM Task A: #{msg}"
    end
end

B.耙子看起来像这样

namespace :b do
    desc "Task B"
    task(:b=>:environment)do
      msg('I AM TASK B')
    end
    def msg(msg)
      puts "MSG SENT FROM Task B: #{msg}"
    end
end

然后当我运行任务 a

rake a RAILS_ENV=sandbox

输出为"从任务 B 发送的消息:我是任务 A">

因此,不会调用 a.rake 中定义的 msg(( 帮助程序方法。而是调用 b.rake 中定义的那个。(更重要的是,如果我有一个 c.rake - 那么当我运行任务 a 时会调用它的 msg 帮助程序方法。

此方法命名空间是否与已知行为冲突?我本以为命名空间会阻止这种情况。

谢谢

您观察到的是,rake 文件命名空间中的方法重新定义了具有相同名称的先前定义的方法。这样做的原因是 Rake namespace 与 Ruby 命名空间(类或模块(有很大不同,实际上它们仅用作其中定义的任务名称的命名空间而没有其他名称。因此,如果将任务a放置在 a 命名空间中,则任务将成为任务a:a,但任务外部的其他代码共享相同的全局命名空间。

这一事实,以及 Rake 在运行给定任务之前加载所有任务的事实,解释了为什么该方法被重新定义。

TL;DR:名称冲突的解决方案/提示

您不能期望放置在单独的namespace但外部任务中的两个具有相同名称(或任何其他代码(的方法将正常工作。尽管如此,这里有一些提示可以解决这种情况:

  • 将方法放在任务中。如果在a:ab:b任务中定义了两个msg方法,则两个 rake 任务都将正常运行并显示预期的消息。

  • 如果您需要在多个 rake 任务中使用 rake namespace中的代码,请将方法/代码提取到真正的 Ruby 命名空间(例如两个模块(,并将代码include到需要它的任务中。考虑对示例耙子的重写:

    # lib/tasks/a.rake:
    module HelperMethodsA
      def msg(msg)
        puts "MSG SENT FROM Task A: #{msg}"
      end
    end
    namespace :a do
      desc "Task A"
      task(:a => :environment) do
        include HelperMethodsA
        msg('I AM TASK A')
      end
    end
    # lib/tasks/b.rake:
    module HelperMethodsB
      def msg(msg)
        puts "MSG SENT FROM Task B: #{msg}"
      end
    end
    namespace :b do
      desc "Task B"
      task(:b => :environment) do
        include HelperMethodsB
        msg('I AM TASK B')
      end
    end
    

    由于这两个模块具有不同的名称,并且因为它们在各自的任务中include,因此两个 rake 任务将再次按预期运行。

现在,让我们借助源代码来证明上述主张......

证明 Rake 首先加载所有任务以及为什么这样做

这个很容易。在主Rakefile中,您始终可以找到以下行:

Rails.application.load_tasks

此方法最终从 Rails 引擎调用以下代码:

def run_tasks_blocks(*) #:nodoc:
  super
  paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end

因此,它会在lib/tasks目录中搜索所有 rake 文件,并按排序顺序一个接一个地加载它们。这就是为什么b.rake文件将在a.rake后加载,并且其中的任何内容都可能重新定义a.rake和所有先前加载的 rake 文件中的代码。

Rake 必须加载所有 rake 文件,因为 rake namespace 名称不必与 rake 文件名相同,因此无法从任务/命名空间名称推断出 rake 文件名。

证明 rake 的namespace不构成真正的类似 Ruby 的命名空间

加载 rake 文件后,将执行 Rake DSL 语句以及 namespace 方法。该方法获取其中定义的代码块,并在Rake.application对象的上下文中执行它(使用yield(,该对象是所有 rake 任务之间共享的 Rake::Application 类的单例对象。没有为命名空间创建动态模块/类,它只是在主对象上下文中执行。

# this reuses the shared Rake.application object and executes the namespace code inside it
def namespace(name=nil, &block)
  # ...
  Rake.application.in_namespace(name, &block)
end
# the namespace block is yielded in the context of Rake.application shared object
def in_namespace(name)
  # ...
  @scope = Scope.new(name, @scope)
  ns = NameSpace.new(self, @scope)
  yield(ns)
  # ...
end

在此处和此处查看相关来源。

Rake 任务确实构成了 ruby 命名空间

但是,对于耙任务本身,情况就不同了。对于每个任务,将创建Rake::Task类(或类似类(的单独对象,并且任务代码在此对象的上下文中运行。对象的创建是在任务管理器的intern方法中完成的:

def intern(task_class, task_name)
  @tasks[task_name.to_s] ||= task_class.new(task_name, self)
end

耙子作者的引述

最后,所有这一切都在 github 上的这个有趣的讨论中得到了证实,该讨论处理了一个非常相似和相关的问题,我们可以从中引用 Rake 的原作者 Jim Weirich:

由于命名空间不引入真正的方法作用域,因此作用域的唯一真正可能性是 DSL 模块。

也许,有一天,Rake 命名空间将成为完整的类范围的实体,有一个地方可以挂起懒惰的定义,但我们还没有到达那里。

使用如下所示的命名空间:

namespace :rake_a do
  desc "Task A"
  task(:a=>:environment)do
    msg('I AM TASK A')
  end
  def msg(msg)
    puts "MSG SENT FROM Task A: #{msg}"
  end
end

rake 命名空间仅用于 rake 任务。请参阅 Rake 文档: The NameSpace class will lookup task names in the the scope defined by a namespace command.

您可以创建一个模块以及您的 rake 命名空间来解决此问题:

module A do
    module_functions
    def msg(msg)
      puts "MSG SENT FROM Task A: #{msg}"
    end
end
namespace :a do
    desc "Task A"
    task(:a=>:environment)do
       A.msg('I AM TASK A')
    end
end