开发者

How do I use Ruby metaprogramming to refactor this common code?

I inherited a project with a lot of badly-written Rake tasks that I need to clean up a bit. Because the Rakefiles are enormous and often prone to bizarre nonsensical dependencies, I'm simplifying and isolating things a bit by refactoring everything to classes.

Specifically, that pattern is the following:

namespace :foobar do
  desc "Frozz the foobar."
  task :frozzify do
    unless Rake.application.lookup('_frozzify')
      require 'tasks/foobar'
      Foobar.new.frozzify
    end
    Rake.application['_frozzify'].invoke
  end

  # Above pattern repeats many times.
end

# Several namespaces, each with tasks that follow this pattern.

In tasks/foobar.rb, I have something that looks like this:

class Foobar
  def frozzify()
    # The real work happens here.
  end

  # ... Other tasks also in the :foobar namespace.
end

For me, this is great, because it allows me to separate the task dependencies from each other and to move them to another location entirely, and I've been able to drastically simplify things and isolate the dependencies. The Rakefile doesn't hit a require until you actually try to run a task. Previously this was causing serious issues because you couldn't even list the tasks without it blowing up.

My problem is that I'm repeating this idiom very frequently. Notice the following patterns:

  • For every namespace :xyz_abc, there is a corresponding class in tasks/... in the file tasks/[namespace].rb, with a class name that looks like XyzAbc.

  • For every task in a particular namespace, there is an identically named method in the associated namespace class. For example, if namespace :foo_bar has a task :apples, you would expect to see def apples() ... inside the FooBar class, which itself is in tasks/foo_bar.rb.

  • Every task :t defines a "meta-task" _t (that is, the task name prefixed w开发者_StackOverflow社区ith an underscore) which is used to do the actual work.

I still want to be able to specify a desc-description for the tasks I define, and that will be different for each task. And, of course, I have a small number of tasks that don't follow the above pattern at all, so I'll be specifying those manually in my Rakefile.

I'm sure that this can be refactored in some way so that I don't have to keep repeating the same idiom over and over, but I lack the experience to see how it could be done. Can someone give me an assist?


Something like this should work for you.

# Declaration of all namespaces with associated tasks.
task_lists = {
  :foobar => {
    :task_one => "Description one",
    :task_two => "Description two",
    :frozzify => "Frozz the foobar",
    :task_three => "Description three" },
  :namespace_two => {
    :task_four => "Description four",
    :etc => "..."} }

# For every namespace with list of tasks...
task_lists.each do |ns, tasks|
  # In the given the namespace...
  namespace ns do
    # For every task in the task list...
    tasks.each do |task_name, description|
      # Set the task description.
      desc description
      # Define the task.
      task task_name do
        unless Rake.application.lookup("_#{task_name}")
          # Require the external file identified by the namespace.
          require "tasks/#{ns}"
          # Convert the namespace to a class name and retrieve its associated
          # constant (:foo_bar will be converted to FooBar).
          klass = Object.const_get(ns.to_s.gsub(/(^|_)(.)/) { $2.upcase })
          # Create a new instance of the class and invoke the method
          # identified by the current task.
          klass.new.send(task_name)
        end
        Rake.application["_#{task_name}"].invoke
      end
    end
  end
end

Update: added descriptions.

(Note that I haven't tested it, so there might be small errors in there.)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜