如何通过元编程来解决这个问题?

时间:2014-06-13 11:11:08

标签: ruby metaprogramming

假设我有以下内容:

class MyWrapper
  def self.wraps_methods(mtd)
     mtd = Array.wrap(mtd)
     mtd.each do |m|
      new_name = m.to_sym
      old_name = "old_#{m}".to_sym
      alias_method old_name, new_name
      class_eval do
        define_method(new_name) do
          begin
            send(old_name)
          rescue StandardError => e
            # do something with error
          end
        end
      end
    end
  end
end

和一个继承自:

的类
   class MyChildClass < MyWrapper
     wraps_methods :first, :second

     def first
       # does something
     end

     def second
       # does something else
     end
   end

问题是,如果我在声明firstsecond之后放了wraps_methods,就像这样:

   class MyChildClass < MyWrapper    
     def first
       # does something
     end

     def second
       # does something else
     end

     wraps_methods :first, :second

   end

然后,它有效。如果我在开头保留wraps_methods,我会得到未定义的方法。我知道这是因为解释器还没有评估第一个和第二个的代码,但是有没有办法进行某种延迟/延迟评估,这样我可以在声明第一个和第二个方法之前保留wraps_methods?< / p>

2 个答案:

答案 0 :(得分:2)

从2.0开始,你可以预装一个模块,有效地使它成为前置类的代理。

在下面的示例中,调用扩展模块模块的方法,传递要包装的方法的名称。对于每个方法名称,都会创建并预先添加一个新模块。这是为了简化代码。您还可以将多个方法附加到单个代理。

当然,您可以使用父类的方法来执行相同的操作。

使用alias_method和instance_method的解决方案的一个重要区别是,您可以在定义方法之前定义要包装的方法。

module Prepender

  def wrap_me(*method_names)
    method_names.each do |m|
      proxy = Module.new do
        define_method(m) do |*args|
          puts "the method '#{m}' is about to be called"
          super *args
        end
      end
      self.prepend proxy
    end
  end
end

使用:

class Dogbert
  extend Prepender

  wrap_me :bark, :deny

  def bark
    puts 'Bah!'
  end

  def deny
    puts 'You have no proof!'
  end
end

Dogbert.new.deny

# => the method 'deny' is about to be called
# => You have no proof!

作为一种懒惰的评估,您可以利用Module#method_added。

的强大功能

答案 1 :(得分:1)

我得到了它:

class MyWrapper
  def self.wraps_methods(*mtd)
    @wrapped_methods = mtd
    mtd.each do |m|
      wrap_method(m) if instance_methods.include?(m)
    end
  end

  def self.wrap_method(m)
    new_name = m.to_sym
    old_name = "old_#{m}".to_sym
    alias_method old_name, new_name
    class_eval do
      Thread.exclusive do
        @skip_method_added = true
        define_method(new_name) do
          begin
            send(old_name)
          rescue StandardError => e
            puts e
          end
        end
        @skip_method_added = false
      end
    end
  end

  def self.method_added(m)
    return if @skip_method_added
    wrap_method(m) if @wrapped_methods.include?(m)
  end

  def self.inherited(klass)
    klass.instance_variable_set('@wrapped_methods', [])
  end
end

class A < MyWrapper
  wraps_methods :foo
  def foo
    raise 'hello'
  end
end

A.new.foo      #=> hello