Ruby
Rails
rake

rake task でメソッド定義

More than 3 years have passed since last update.

なんか共通の処理があって、自然な発想としてメソッドを定義してしまうことがあると思う。

以下の様な rake task 宣言を見て欲しい。例なのでDRYじゃないとか言わないでね。

namespace :report do
  def report #1
    # hogehoge
  end

  task :show do
    p report
  end
  task :mail do
    mail(body: report)
  end
end

namespace :report2 do
  def report #2
    # fugafuga
  end

  task :show do
    p report
  end
  task :mail do
    mail(body: report)
  end
end

ここで、 report という名前のメソッドを2回定義しているが、 namespace が分かれてるから大丈夫だって、思うだろう。

namespace 分かれてない。

分かれてません。どっちも main(Object) に宣言されてしまう。
rake report:show ってすると report #2 のほうが呼ばれて fugafuga されてしまうのだ。

仕方ないので module に入れるべ、ってんで、若い頃の僕はこんなことをした。

namespace :report do
  module ReportTask
    def report #1
      # hogehoge
    end
  end

  task :show do
    include ReportTask
    p report
  end
  task :mail do
    include ReportTask
    mail(body: report)
  end
end

namespace :report2 do
  module ReportTask2
    def report #2
      # fugafuga
    end
  end

  task :show do
    include ReportTask2
    p report
  end
  task :mail do
    include ReportTask2
    mail(body: report)
  end
end

これで望みのメソッドを確実に呼べる。めでたしめでたし。
でもなかった。

include しちゃ意味ない

よく考えればわかることだが、 include ReportTask が何しているかってえと結局 main(Object) に include しちゃうのである。
幸い今回の例を普通に rake タスクとして使うぶんには問題は発生しないが、

task :report_all => ['report:mail', 'report2:mail']

などという横断的依存関係を持つタスクが現れた時点で破綻してしまう。

やはりここは

全体を module に

入れちまうのが安全というものだ。

module ReportTask
  extend Rake::DSL
  extend self

  namespace :report do
    def report #1
      # hogehoge
    end

    task :show do
      p report
    end
    task :mail do
      mail(body: report)
    end
  end
end

module ReportTask2
  extend Rake::DSL
  extend self

  namespace :report2 do
    def report #2
      # fugafuga
    end

    task :show do
      p report
    end
    task :mail do
      mail(body: report)
    end
  end
end

こうするとばっちりモジュールが分かれて安全安心になる。 extend Rake::DSLtask等のキーワードをモジュール内でも使用するためのもの、extend selfdef self.report と本来書くべきところを手抜きするおまじないである。
どうせ他の場所から呼ばれるものではないし、干渉しない名前を考えるのも面倒なのでいっそ無名モジュールでも良い。

Module.new do
  extend Rake::DSL
  extend self

  namespace :report

9/11 追記 無名モジュール定義時は全ての定数に self:: が必要。 http://qiita.com/kuboon/items/dc5f30abee136abcd319

しかし、この仕事、 namespace がそもそもやってくれてもいいよねー?って思えてならないのでちょっと黒魔法チックな解決も一応ご用意した。

namespace を上書き定義してしまえ

Rakefile
require 'rake'

# これが元 https://github.com/ruby/rake/blob/v10.4.2/lib/rake/task_manager.rb#L205
module Rake
  module TaskManager
    def in_namespace(name, &block)
      name ||= generate_name
      @scope = Scope.new(name, @scope)
      ns = NameSpace.new(self, @scope)
      Module.new do
        self.extend Rake::DSL
        instance_eval(&block)
      end
      ns
    ensure
      @scope = @scope.tail
    end
  end
end

Admin::Application.load_tasks

これで、各 namespace は個別の Module 内で eval されるので全て期待通りに動く (多分)。
namespace の nest とかは全く考慮していないし、同じ namespace でもセクションが別だと名前空間が分かれてしまうみたいな問題があるけど、まあ大体のケースで動くし、もし nest等 が必要になったら include 使って回避するといいと思う。