【问题标题】:Before and after event handlers in RubyRuby 中的事件处理程序之前和之后
【发布时间】:2019-12-23 22:44:40
【问题描述】:

有没有办法监视对象,以便在调用该对象的特定方法之前和/或之后运行块或 Lamba?例如,像这样:

watch(lionel, :say_you, :before) do
    puts '[before say_you]'
end

lionel.say_you()
# outputs [before say_you]

我的要求的一个重要部分是我根本不想修补对象。对象不应有任何更改。我只想看它,而不是改变它(海森堡会很自豪)。

我已经编写了一个模块,可以实现我所描述的功能。不幸的是,它有一些不好的副作用:它会减慢系统速度,并且永远不会清理它的对象 ID 散列。所以我不会在生产中使用它,但它展示了观察对象而不用猴子修补它的概念。

# watcher module
# (Adrian is one of The Watchmen)
module Adrian
   @registry = {}
   EVENT_IDS = {:before => :call, :after => :return}

   # watch
   def self.watch(obj, method, event_id, &handler)
      # get event type
      event = EVENT_IDS[event_id]
      event or raise 'unknown-event: unknown event param'

      # get object id
      obj_id = obj.object_id

      # store handler
      @registry[obj_id] ||= {}
      @registry[obj_id][method] ||= {}
      @registry[obj_id][method][event] ||= []
      @registry[obj_id][method][event].push(handler)
   end

   # trace
   TracePoint.trace(:call, :return) do |tp|
      # get watched object or return
      if handlers = @registry[tp.binding.receiver.object_id]
         handle tp, handlers
      end
   end

   # handle
   def self.handle(tp, handlers)
      # $tm.hrm

      # if this method is watched
      callee = handlers[tp.callee_id]
      callee or return

      # get blocks
      blocks = callee[tp.event]
      blocks or return

      # loop through series
      blocks.each do |block|
         block.call
      end
   end
end

# Lionel class
class Lionel
   def say_you
      puts 'you'
   end

   def say_me
      puts 'me'
   end
end

# instance
lionel = Lionel.new()

# before
Adrian.watch(lionel, :say_you, :before) do
   puts '[before say_you]'
end

# after
Adrian.watch(lionel, :say_me, :after) do
   puts '[after say_me]'
end

# call method
lionel.say_you
lionel.say_me

输出:

[before say_you]
you
me
[after say_me]

【问题讨论】:

  • 为什么不给对象打补丁呢?这就是这种类型的仪器通常是如何完成的。这个,或者装饰器(但是装饰一个对象会生成一个新的对象,这可能不是你想要的)。
  • @tadman: lionel 是一个本地变量。
  • @SergioTulentsev 哦,在那种情况下,这真的不切实际。如果没有一些黑魔法,你就无法包装对变量的引用。
  • @tadman:我会使用一些 MP 魔法,是的。但 Mike 设计了一种基于跟踪点的解决方案,即无补丁。有自己的缺点。

标签: ruby


【解决方案1】:

这是一个使用单例类的解决方案,用于您要观看的对象。我不确定这是好的解决方案,但它可能至少有趣

watch 方法采用对象、方法名称和顺序参数以及一个块。 order 参数指定何时运行该块,即在被监视方法之前、之后或周围。

watch方法保存原始实例方法,然后在对象上定义一个与原始方法同名的单例方法。单例方法在 order 参数指定的一个或多个时间调用传递的块。单例方法还会在适当的时候调用与对象绑定的原始实例方法。

也许有一种更好的方法可以在单例方法中调用实例方法,但我还没有找到。我也不确定这将如何处理各种参数类型,即关键字参数等。

这里是 watch 方法。

def watch(obj,method_name,order,&block)
    inst_method = obj.class.instance_method(method_name)
    obj.define_singleton_method(method_name) do |*args|
        block.call if [:before,:around].include?(order)
        inst_method.bind(self).call(*args) 
        block.call if [:after,:around].include?(order)
    end    
end

用法示例:

class Foo 
    def initialize(type)
        @type = type
    end

    def i_method(arg)
        puts "#{@type} instance method called with arg = #{arg}"
    end
end

watched_inst = Foo.new("watched")
unwatched_inst = Foo.new("unwatched") 

watch(watched_inst,:i_method,:before) do 
   puts "do before"
end

watched_inst.i_method("foo")
unwatched_inst.i_method("bar")
watched_inst.i_method("foo_bar")

watch(watched_inst,:i_method,:after) do 
   puts "do after"
end

watched_inst.i_method("hello")

watch(watched_inst,:i_method,:around) do 
   puts "do around"
end

watched_inst.i_method("hello")

unwatched_inst.i_method("foo")

输出:

do before
watched instance method called with arg = foo
unwatched instance method called with arg = bar
do before
watched instance method called with arg = foo_bar
watched instance method called with arg = hello
do after
do around
watched instance method called with arg = hello
do around
unwatched instance method called with arg = foo

【讨论】:

  • 非常有趣!这不是我想要的,但它非常接近。它现在可能会起作用。谢谢!
猜你喜欢
  • 1970-01-01
  • 2019-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-05
  • 2012-01-19
  • 2019-10-28
相关资源
最近更新 更多