首页 > 解决方案 > 包装一个方法,并将一个实例变量传递给它

问题描述

我正在尝试定义一个包含另一种方法的宏,然后将实例传递给宏。这是一个人为的例子......

module Wrapper
  def wrap(method_name, options: {})
    class_eval do
      new_method = "_wrap_#{method_name}"
      alias_method new_method, method_name

      define_method method_name do |*args, &block|
        # binding.pry
        puts "Options... #{options}"
        
        send new_method, *args, &block
      end
    end
  end
end

class Thing
  extend Wrapper

  class << self
    extend Wrapper

    def bar
      puts 'Bar!'
    end
    wrap :bar, options: -> { bar_options }

    private

    def bar_options
      { bar: 2 }
    end
  end

  def foo
    puts 'Foo!'
  end
  wrap :foo, options: -> { foo_options }

  private

  def foo_options
    { foo: 1 }
  end
end

所以,我知道我可以从内部访问实例define_method,但我认为我不应该在我的类中创建/重命名/别名方法以符合模块 - 我想传递配置,所以说话。我觉得instance_exec/eval是我的朋友,在这里,但我似乎无法得到正确的咒语。我也尝试将块传递给代码,但yield行为与 proc 相同。也许binding吧,但出于某种原因,我根本无法理解这一点。

这是在define_method通话中...

> Thing.new.foo
=> Options... #<Proc:0x00007fcb0782cfd8@(irb):24>
Foo!

> self.class
=> Thing

> self
=> #<Thing:0x00007fdfb78b88e8>

> options
=> #<Proc:0x00007fcb0782cfd8@(irb):24>

> options.call
# NameError: undefined local variable or method `foo_options` for Thing:Class

> foo_options
=> {:foo=>1}

> self.class.instance_eval &options
# NameError: undefined local variable or method `foo_options` for Thing:Class

> self.class.instance_exec &options
# NameError: undefined local variable or method `foo_options` for Thing:Class

我了解 proc 如何捕获范围以供以后使用,因此我可以在这里看到 proc/lambda 的使用是如何不正确的。当类加载时,该wrap方法“包装”该方法并foo_options在类级别捕获以供稍后调用 - 这在类级别不存在。调用options: foo_options做同样的事情,只是在负载时爆炸。

任何一点帮助...谢谢!

标签: rubymetaprogramming

解决方案


我是如此接近......在 内define_method,我可以访问实例,并且可以调用instance_execself,而不是 self.class!此外,自 ruby​​ 2.0 以来,还有一种更新的、首选的包装方法。

module Wrapper
  def wrap(method_name, options: {})
    proxy = Module.new
    proxy.define_method(method_name) do |*args, &block|
      options = instance_exec(&options) if options.is_a?(Proc)
      target = is_a?(Module) ? "#{self}." : "#{self.class}#"
      puts "#{target}#{method_name} is about to be called. `wrap` options #{options}"
      super *args, &block
    end
    self.prepend proxy
  end
end

输出:

> Thing.new.foo
Thing#foo is about to be called. `wrap` options {:foo=>1}
Foo!
=> nil

> Thing.bar
Thing.bar is about to be called. `wrap` options {:bar=>2}
Bar!
=> nil

这比 Sergio 在评论中提到的“旧方式”要干净得多。这个问题帮助了我!

这种方法的另一个巨大好处是您可以在文件顶部定义宏(可以说是它们所属的位置)。


推荐阅读