13

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 free 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?

Jamie Wyneski
  • 131
  • 1
  • 1
  • 4

3 Answers3

26

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!
kostja
  • 60,521
  • 48
  • 179
  • 224
  • Your Prepender example unnecessarily creates/prepends a module for each wrapped method name; it'd probably make more sense to define all the methods on a single module and just prepend it once. – Alexander Wallace Matchneer Mar 26 '19 at 16:49
  • Definitely. This is what I say in the answer - third paragraph - suboptimal example for code simplicity. – kostja Mar 26 '19 at 17:07
  • Sorry about that, I didn't read the explanation closely. That said, I'm not sure it makes sense to leave the code as it is for the sake of "simplicity". It's not really simple, it's just strange. – Alexander Wallace Matchneer Mar 27 '19 at 18:02
  • can you provide working example without multiple modules? It doesn't seem work for me – Filip Bartuzi Nov 28 '19 at 16:37
  • 1
    @FilipBartuzi Should work if you move the `proxy = Module.new` to before `method_names.each`, the `self.prepend proxy` to after, and say `proxy.define_method` instead of just `define_method`. Then you're only making one proxy module. – mu is too short Nov 29 '19 at 20:45
9

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.

horseyguy
  • 29,455
  • 20
  • 103
  • 145
Jakub Hampl
  • 39,863
  • 10
  • 77
  • 106
  • What if :method takes a block? – Jamie Wyneski Nov 18 '10 at 20:47
  • You don't have to use `define_method`, you could do an `eval` to define the method. Eg. `eval "def #{method}(*args, &block) ...` – Jakub Hampl Nov 18 '10 at 20:49
  • This requires that `notify_when` comes after the method definition, so it will not work on your code example. Also it *does* modify the `bar` method. But aside from using a proxy object you *have* to modify (decorate) your methods. – horseyguy Nov 18 '10 at 21:05
3

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.

horseyguy
  • 29,455
  • 20
  • 103
  • 145
  • Could you please add an explanation, why rebinding the original method is better than using alias_method? The difference is not obvious to me. Thank you. – kostja Jun 24 '14 at 19:43
  • 1
    Nevermind, I have found [this nice explanation](http://stackoverflow.com/questions/4470108). – kostja Jun 24 '14 at 19:52