Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 206
Jamie Wyneski Avatar asked Nov 18 '10 20:11

Jamie Wyneski


People also ask

How do you call a module method in Ruby?

As with class methods, you call a module method by preceding its name with the module's name and a period, and you reference a constant using the module name and two colons.

How do you access a module method in Ruby?

A user cannot access instance method directly with the use of the dot operator as he cannot make the instance of the module. To access the instance method defined inside the module, the user has to include the module inside a class and then use the class instance to access that method.

How do I use modules in Ruby?

Creating Modules in Ruby To define a module, use the module keyword, give it a name and then finish with an end . The module name follows the same rules as class names. The name is a constant and should start with a capital letter. If the module is two words it should be camel case (e.g MyModule).


3 Answers

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!
like image 189
kostja Avatar answered Sep 25 '22 00:09

kostja


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.

like image 29
Jakub Hampl Avatar answered Sep 26 '22 00:09

Jakub Hampl


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.

like image 35
horseyguy Avatar answered Sep 26 '22 00:09

horseyguy