Edited at

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 使って回避するといいと思う。