Executing code for every method call in a Ruby module

24,925

Solution 1

Like this:

module M
  def self.before(*names)
    names.each do |name|
      m = instance_method(name)
      define_method(name) do |*args, &block|  
        yield
        m.bind(self).(*args, &block)
      end
    end
  end
end

module M
  def hello
    puts "yo"
  end

  def bye
    puts "bum"
  end

  before(*instance_methods) { puts "start" }
end

class C
  include M
end

C.new.bye #=> "start" "bum"
C.new.hello #=> "start" "yo"

Solution 2

This is exactly what aspector is created for.

With aspector you don't need to write the boilerplate metaprogramming code. You can even go one step further to extract the common logic into a separate aspect class and test it independently.

require 'aspector'

module MyModule
  aspector do
    before :go_forth, :add_multiply do
      ...
    end
  end

  def go_forth
    # code particular to this method follows ...
  end

  def and_multiply
    # then something completely different ...
  end
end

Solution 3

You can implement it with method_missing through proxy Module, like this:

module MyModule

  module MyRealModule
    def self.go_forth
      puts "it works!"
      # code particular to this method follows ...
    end

    def self.and_multiply
      puts "it works!"
      # then something completely different ...
    end
  end

  def self.method_missing(m, *args, &block)
    reused_statement
    if MyModule::MyRealModule.methods.include?( m.to_s )
      MyModule::MyRealModule.send(m)
    else
      super
    end
  end

  def self.reused_statement
    puts "reused statement"
  end
end

MyModule.go_forth
#=> it works!
MyModule.stop_forth
#=> NoMethodError...

Solution 4

You can do this by metaprogramming technique, here's an example:

module YourModule
  def included(mod)
    def mod.method_added(name)
      return if @added 
      @added = true
      original_method = "original #{name}"
      alias_method original_method, name
      define_method(name) do |*args|
        reused_statement
        result = send original_method, *args
        puts "The method #{name} called!"
        result
      end
      @added = false
    end
  end

  def reused_statement
  end
end

module MyModule
  include YourModule

  def go_forth
  end

  def and_multiply
  end
end

works only in ruby 1.9 and higher

UPDATE: and also can't use block, i.e. no yield in instance methods

Solution 5

I dunno, why I was downvoted - but a proper AOP framework is better than meta-programming hackery. And thats what OP was trying to achieve.

http://debasishg.blogspot.com/2006/06/does-ruby-need-aop.html

Another Solution could be:

module Aop
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def before_filter(method_name, options = {})
      aop_methods = Array(options[:only]).compact
      return if aop_methods.empty?
      aop_methods.each do |m|
        alias_method "#{m}_old", m
        class_eval <<-RUBY,__FILE__,__LINE__ + 1
          def #{m}
            #{method_name}
            #{m}_old
          end
        RUBY
      end
    end
  end
end

module Bar
  def hello
    puts "Running hello world"
  end
end

class Foo
  include Bar
  def find_hello
    puts "Running find hello"
  end
  include Aop
  before_filter :find_hello, :only => :hello
end

a = Foo.new()
a.hello()
Share:
24,925
GladstoneKeep
Author by

GladstoneKeep

Software engineer

Updated on July 11, 2022

Comments

  • GladstoneKeep
    GladstoneKeep almost 2 years

    I'm writing a module in Ruby 1.9.2 that defines several methods. When any of these methods is called, I want each of them to execute a certain statement first.

    module MyModule
      def go_forth
        a re-used statement
        # code particular to this method follows ...
      end
    
      def and_multiply
        a re-used statement
        # then something completely different ...
      end
    end
    

    But I want to avoid putting that a re-used statement code explicitly in every single method. Is there a way to do so?

    (If it matters, a re-used statement will have each method, when called, print its own name. It will do so via some variant of puts __method__.)

  • fl00r
    fl00r about 13 years
    +1 Like it. But Ruby 1.8.7 doesn't support it? NoMethodError: undefined method before' for M:Module`
  • horseyguy
    horseyguy about 13 years
    @fl00r, all you should have to change to have it work in 1.8.7 is the proc invocation syntax, i'm using .() (which is 1.9 only) rather than .call()
  • nurettin
    nurettin over 11 years
    I don't know why this was downvoted, either. Perhaps it was because there was no example just a link.
  • reizals
    reizals almost 10 years
    Hi, could you explain me what exactly m.bind(self).(*args, &block) do? I've search the ruby documentation and many pages from google, but I still don't know how it works. Many thx for help.
  • konsolebox
    konsolebox almost 10 years
    @reizals See ruby-doc.org/core-2.1.2/UnboundMethod.html#method-i-bind. (Reply is just for everyone's reference.)
  • hachpai
    hachpai over 9 years
    So, the location of the bind is important? We can't make it at the beggining of the class definition?
  • DeejUK
    DeejUK almost 9 years
    Downvoting for link to random library without any explanation as to why I should click the link
  • Michael K Madison
    Michael K Madison over 6 years
    Dude, this is beautiful. Thanks for sharing this!
  • Michael K Madison
    Michael K Madison over 6 years
    @horseyguy Wait, are we defining an overriding method? So before needs to be defined after the original method?
  • Michael K Madison
    Michael K Madison over 6 years
    I didn't even know about .(), nice.
  • Michael K Madison
    Michael K Madison about 6 years
    Wait, why doesn't this work if you use the before method in the C class definition? if you move before(*instance_methods) { puts "start " } to C class I get <class:C>': undefined method before' for C:Class (NoMethodError)`
  • Rasna Shakya
    Rasna Shakya about 2 years
    How to pass arguments onto it?