开发者

How do I wrap the invocation of a Ruby method by including a module?

开发者 https://www.devze.com 2023-01-25 15:46 出处:网络
I want to be notified when certain things happen in some of my classes. I want to set this up in such a way that the implementation of my methods in those classes doesn\'t change.

I want to be notified when certain things happen in some of my classes. I want to set this up in such a way that the implementation of my methods in those classes doesn't change.

I was thinking I'd have something like the following module:

module Notifications
  extend ActiveSupport::Concern

  module ClassMethods
    def notify_when(method)
      puts "the #{method} method was called!"
      # additional suitable notification code
      # now, run the method indicated by the `method` argument
    end
  end
end

Then I can mix it into my classes like so:

class Foo
  include Notifications

  # notify that we're running :bar, then run bar
  notify_when :bar

  def bar(...)  # bar may have any arbitrary signature
    # ...
  end
end

My key desire is that I don't want to have to modify :bar to get notifications working correctly. Can this be done? If so, how would I write the notify_when implementation?

Also, I'm using Rails 3, so if there are ActiveSupport or other techniques I can use, please feel fre开发者_运维知识库e to share. (I looked at ActiveSupport::Notifications, but that would require me to modify the bar method.)


It has come to my attention that I might want to use "the Module+super trick". I'm not sure what this is -- perhaps someone can enlighten me?


It has been quite a while since this question here has been active, but there is another possibility to wrap methods by an included (or extended) Module.

Since 2.0 you can prepend a Module, effectively making it a proxy for the prepending class.

In the example below, a method of an extended module module is called, passing the names of the methods you want to be wrapped. For each of the method names, a new Module is created and prepended. This is for code simplicity. You can also append multiple methods to a single proxy.

An important difference to the solutions using alias_method and instance_method which is later bound on self is that you can define the methods to be wrapped before the methods themselves are defined.

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

Use:

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!


I imagine you could use an alias method chain.

Something like this:

def notify_when(method)  
  alias_method "#{method}_without_notification", method
  define_method method do |*args|
    puts "#{method} called"
    send "#{method}_without_notification", args
  end
end

You do not have to modify methods yourself with this approach.


I can think of two approaches:

(1) Decorate the Foo methods to include a notification.

(2) Use a proxy object that intercepts method calls to Foo and notifies you when they happen

The first solution is the approach taken by Jakub, though the alias_method solution is not the best way to achieve this, use this instead:

def notify_when(meth)  
  orig_meth = instance_method(meth)
  define_method(meth) do |*args, &block|
    puts "#{meth} called"
    orig_meth.bind(self).call *args, &block
  end
end

The second solution requires you to use method_missing in combination with a proxy:

class Interceptor
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    if @target.respond_to?(name)
      puts "about to run #{name}"
      @target.send(name, *args, &block)
    else
      super
    end
  end
end

class Hello; def hello; puts "hello!"; end; end

i = Interceptor.new(Hello.new)
i.hello #=> "about to run hello"
        #=> "hello!"

The first method requires modifying the methods (something you said you didn't want) and the second method requires using a proxy, maybe something you do not want. There is no easy solution I'm sorry.

0

精彩评论

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