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?

3 Answers 3

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! 
Sign up to request clarification or add additional context in comments.

5 Comments

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.
Definitely. This is what I say in the answer - third paragraph - suboptimal example for code simplicity.
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.
can you provide working example without multiple modules? It doesn't seem work for me
@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.
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.

3 Comments

What if :method takes a block?
You don't have to use define_method, you could do an eval to define the method. Eg. eval "def #{method}(*args, &block) ...
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.
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.

2 Comments

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.
Nevermind, I have found this nice explanation.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.