D1535c766311cdf0dddf2269b6cd1120

The following module provides method hooks (code that is called after a method has been invoked) for specified methods. Is that readable of general use for you? For more examples and unit tests please follow the discussion at http://cheind.blogspot.com/2008/12/method-hooks-in-ruby.html . Criticism welcome! Thanks!

#
# Project:: Ruby-Snippets
# 
# Author:: Christoph Heindl  (mailto:christoph.heindl@gmail.com)
# Homepage:: http://cheind.blogspot.com
#
# == Overview
# 
# Implements <tt>FollowingHook#following</tt>. A
# utility to hook into instance methods and execute
# custom code after hooked methods are called.
#
# See discussion at
# http://cheind.blogspot.com/2008/12/method-hooks-in-ruby.html
#


# Contains methods to hook method calls
module FollowingHook
  
  module ClassMethods
    
    private
    
    # Hook the provided instance methods so that the block 
    # is executed directly after the specified methods have been invoked.
    #
    # There can only be one hook for a single instance method. Further attempts
    # to hook already hooked methods will be silently ignored.
    #
    # +syms+ is list of method symbols or stringified names to hook on
    # +block+ is the block to execute after hooked method has been invoked.
    # Block will receive two arguments: the receiver of the method and an argument hash
    # which contains the invoked method name <tt>:method</tt>, the arguments passed to
    # the method <tt>:args</tt> and the return value of the method <tt>:return</tt>.
    #
    #   class Object
    #     include FollowingHook
    #     following :system do |receiver, args|
    #       p "#{args[:method]} called with arguments #{args[:args].join(",")}"
    #       p "return value was #{args[:return]}"
    #     end
    #   end
    #
    #   system('ruby --version')
    #   # => ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
    #   # => "system called with arguments ruby --version"
    #   # => "return value was true"
    #   # => true
    #
    def following(*syms, &block)
      syms.each do |sym| # For each symbol
        str_id = "__#{sym}__hooked__"
        unless private_instance_methods.include?(str_id)
          alias_method str_id, sym        # Backup original method
          private str_id                  # Make backup private
          define_method sym do |*args|    # Replace method
            ret = __send__ str_id, *args  # Invoke backup
            block.call(self,              # Invoke hook
              :method => sym, 
              :args => args,
              :return => ret
            )
            ret # Forward return value of method
          end
        end
      end
    end
  end
  
  # On inclusion, we extend the receiver by the defined class-methods
  # This is an ruby idiom for defining class methods within a module.
  def FollowingHook.included(base)
    base.extend(ClassMethods)
  end
end

Refactorings

No refactoring yet !

B19b02a49b433c9e2e6e6c43785d2bfb

raggi

December 8, 2008, December 08, 2008 11:24, permalink

1 rating. Login to rate!

I wouldn't use that, instead if at all, I'd probably use "after" as defined below.

Also, you might want to look into the way task execution occurs in Rake.

I find redefining methods like this tends to become unmanageable when there are other ways to invoke code that don't require permanently damaging the object protocol that you're trying to hook.

Anyway, 1.9 support too.

module FollowingHook
  module ClassMethods

    private
    def following(*syms, &block)
      syms.each do |sym| # For each symbol
        hook_method = RUBY_VERSION >= '1.9.0' ? :"__#{sym}__hooked__" : "__#{sym}__hooked__"
        raise ArgumentError, 'hook already exists' if private_instance_methods.include?(hook_method)
        alias_method hook_method, sym
        private hook_method
        define_method sym do |*args|
          after sym, *args, &block
        end
      end
    end
  end
  
  def after(method, *args)
    yield :method => method, :args => args, :return => send(:"__#{method}__hooked__", *args)
  end

  def self.included(base)
    base.extend(ClassMethods)
  end
end
D1535c766311cdf0dddf2269b6cd1120

Christoph Heindl

December 9, 2008, December 09, 2008 07:44, permalink

No rating. Login to rate!

raggi,

thanks for your input!

I browsed the changes for #alias_method in ruby 1.9 (http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9) but could not find any hints on #alias_method requiring symbols. http://www.ruby-doc.org/core-1.9/index.html misses that part too.

Guess I have to install it :)

I agree that hooking method may produce an environment that is hard to maintain, but sometimes (maybe rarely) hooking is just what gets you the job done quickly and does not cluster your code see contrived example below, don't you think?

Best regards,
christoph

require 'logger'
require 'util/following'

$Log = Logger.new(STDOUT)
$Log.level = Logger::DEBUG

class Window
  attr_accessor :text
  attr_reader :renderer

  if $Log.level <= Logger::DEBUG
    include FollowingHook
    following :text=, :text, :renderer do |sender, args|
      $Log.debug("#{sender} called with arguments #{args[:args].join(',')}")
    end
  end
end
F1e3ab214a976a39cfd713bc93deb10f

Tj Holowaychuk

December 10, 2008, December 10, 2008 17:34, permalink

1 rating. Login to rate!

Not sure if this really is what your looking for but, In similar situations I tend to create a module, and then use the ancestor chain to super back to the original functionality like below. Probably not the best solution either but it worked well for me.

require 'fileutils'

module VerboseFileUtils
  
  include FileUtils
  
  ##
  # Wrap _methods_ with _action_ log message.
    
  def self.log action, *methods
    methods.each do |meth|
      define_method meth do |*args|
        Commander::UI.log "#{action}", *args
        super
      end
    end
  end
  
  log "remove", :rm, :rm_r, :rm_rf, :rmdir
  log "create", :touch, :mkdir, :mkdir_p
  log "copy", :cp, :cp_r
  log "move", :mv
  log "change", :cd
  log "link", :ln, :ln_s
  log "install", :install

end

include VerboseFileUtils
Outputs:

    create  this
  remove  that
      move  foo
        link   bar
F1e3ab214a976a39cfd713bc93deb10f

Tj Holowaychuk

December 10, 2008, December 10, 2008 17:34, permalink

1 rating. Login to rate!

GAH! the output portion did not work at all.. haha you get the point

D1535c766311cdf0dddf2269b6cd1120

Christoph Heindl

December 10, 2008, December 10, 2008 20:23, permalink

No rating. Login to rate!

Using the ancestor chain + overriding existing to call ancestor is a nice idea. Thanks for sharing your thoughts.

21e26ead44b2956ea4081eb355bfab16

Mario Freitas

October 15, 2010, October 15, 2010 04:15, permalink

No rating. Login to rate!

I basically used raggi's code for dealing with hooks and modified it to use the concept of "filters".

#
# Based on raggi's refactored version of the FollowingHook module's code.
# http://refactormycode.com/codes/656-method-hooks-in-ruby-any-cleaner
#
# Main changes:
# - Removed Ruby version test (Ruby 1.8 and 1.9 both work).
# - Support not only "after" but also "before" hooks.
# - Transformed "hooks" into "filters" (allow changing of args, and return values).
# - Allow multiple filters to be installed and uninstalled.
#
module MethodFilter
  module ClassMethods
    def remove_filter (type, *syms)
      @@__filters__ ||= { :before => {}, :after => {} }
      syms.each { |sym| @@__filters__[type][sym] = [] }
    end

    private

    def filter (type, *syms, &block)
      syms.each do |sym|
        @@__filters__ ||= { :before => {}, :after => {} }
        @@__filters__.each_key { |k| @@__filters__[k][sym] ||= [] }
        @@__filters__[type][sym] << block
        filter = "__#{sym}__filter__".to_sym
        next if private_instance_methods.map { |m| m.to_sym }.include?(filter)
        alias_method filter, sym
        private filter
        define_method sym do |*args|
          call = { :method => sym, :args => args }
          @@__filters__[:before][sym].each do |b|
            send :execute_filter, :before, call, &b
            return call[:return] if call.has_key?(:return)
          end
          call[:return] = send filter, *call[:args] 
          @@__filters__[:after][sym].each { |b| send :execute_filter, :after, call, &b }
          return call[:return]
        end
      end
    end

    def before_filter (*syms, &block)
      filter(:before, *syms, &block)
    end

    def after_filter (*syms, &block)
      filter(:after, *syms, &block)
    end
  end

  def execute_filter (type, call)
    call[:type] = type
    yield call, *call[:args]
  end

  def self.included (base)
    base.extend(ClassMethods)
  end
end
#
# Place in same folder as method_filter.rb
#

require File.join(File.dirname(File.expand_path(__FILE__)),"method_filter")

class Foo
  include MethodFilter

  def func (x, y, z)
    puts "func(#{x}, #{y}, #{z})"
    r = x * y + z
    puts "func = #{r}"
    r
  end

  after_filter(:func) do |call, x, y, z|
    puts "Multiplying return value of func by 2"
    call[:return] *= 2
  end

  after_filter(:func) do |call, *args|
    puts "#{call[:method]}'s #{call[:type]} filter called with #{args.inspect}"
  end

  before_filter(:func) do |call, *args|
    puts "#{call[:method]}'s #{call[:type]} filter called with #{args.inspect}"
    # if you want to exit the filters chain and return a value immediately,
    # you can define call[:return] to something (including nil) and that will
    # be returned to the caller.
  end
  
  before_filter(:func) do |call, x, y, z|
    puts "Reversing order of arguments passed to func"
    call[:args] = [ z, y, x ]
  end
end

f = Foo.new
puts "ALL filters enabled"
puts f.func(1,2,3)
puts "AFTER filters disabled"
Foo.remove_filter(:after, :func)
puts f.func(1,2,3)
puts "ALL filters disabled"
Foo.remove_filter(:before, :func)
puts f.func(1,2,3)
ALL filters enabled
func's before filter called with [1, 2, 3]
Reversing order of arguments passed to func
func(3, 2, 1)
func = 7
Multiplying return value of func by 2
func's after filter called with [3, 2, 1]
14
AFTER filters disabled
func's before filter called with [1, 2, 3]
Reversing order of arguments passed to func
func(3, 2, 1)
func = 7
7
ALL filters disabled
func(1, 2, 3)
func = 5
5
21e26ead44b2956ea4081eb355bfab16

Mario Freitas

October 15, 2010, October 15, 2010 07:18, permalink

No rating. Login to rate!

I forgot to include an :instance key in the "call" hash so that you can access the instance's methods.

In method_filter.rb, just change:

call = { :method => sym, :args => args }

to:

call = { :method => sym, :args => args, :instance => self }

and then you can use call[:instance].some_method as if it was "self.some_method" (i.e., execute "some_method" in the context of the instance whose filtered method is being executed).

The fixed code follows below anyway.

#
# Based on raggi's refactored version of the FollowingHook module's code.
# http://refactormycode.com/codes/656-method-hooks-in-ruby-any-cleaner
#
# Main changes:
# - Removed Ruby version test (Ruby 1.8 and 1.9 both work).
# - Support not only "after" but also "before" hooks.
# - Transformed "hooks" into "filters" (allow changing of args, and return values).
# - Allow multiple filters to be installed and uninstalled.
#
module MethodFilter
  module ClassMethods
    def remove_filter (type, *syms)
      @@__filters__ ||= { :before => {}, :after => {} }
      syms.each { |sym| @@__filters__[type][sym] = [] }
    end

    private

    def filter (type, *syms, &block)
      syms.each do |sym|
        @@__filters__ ||= { :before => {}, :after => {} }
        @@__filters__.each_key { |k| @@__filters__[k][sym] ||= [] }
        @@__filters__[type][sym] << block
        filter = "__#{sym}__filter__".to_sym
        next if private_instance_methods.map { |m| m.to_sym }.include?(filter)
        alias_method filter, sym
        private filter
        define_method sym do |*args|
          call = { :method => sym, :args => args, :instance => self }
          @@__filters__[:before][sym].each do |b|
            send :execute_filter, :before, call, &b
            return call[:return] if call.has_key?(:return)
          end
          call[:return] = send filter, *call[:args] 
          @@__filters__[:after][sym].each { |b| send :execute_filter, :after, call, &b }
          return call[:return]
        end
      end
    end

    def before_filter (*syms, &block)
      filter(:before, *syms, &block)
    end

    def after_filter (*syms, &block)
      filter(:after, *syms, &block)
    end
  end

  def execute_filter (type, call)
    call[:type] = type
    yield call, *call[:args]
  end

  def self.included (base)
    base.extend(ClassMethods)
  end
end
21e26ead44b2956ea4081eb355bfab16

Mario Freitas

October 18, 2010, October 18, 2010 02:19, permalink

No rating. Login to rate!

I am sorry for the previous posts, as I was kind in a hurry and had not tested the code carefully.
Fixed several bugs. Should be better now.

#
# Based on raggi's refactored version of the FollowingHook module's code.
# http://refactormycode.com/codes/656-method-hooks-in-ruby-any-cleaner
#
# Main changes:
# - Removed Ruby version test (Ruby 1.8 and 1.9 both work).
# - Support not only "after" but also "before" hooks.
# - Transformed "hooks" into "filters" (allow changing of args, and return values).
# - Allow multiple filters to be installed and uninstalled.
#
module MethodFilter
  module ClassMethods
    def remove_filter (type, *syms)
      syms.each { |sym| __filters__[type][sym] = [] }
    end

    def __filters__
      return class_variable_get(:@@__filters__) if class_variable_defined?(:@@__filters__)
      class_variable_set(:@@__filters__, {:before => {}, :after => {}})
    end

    private

    def filter (type, *syms, &block)
      syms.each do |sym|
        __filters__.each_key { |k| __filters__[k][sym] ||= [] }
        __filters__[type][sym] << block
        filter = "__#{sym}__filter__".to_sym
        next if private_instance_methods.map { |m| m.to_sym }.include?(filter)
        alias_method filter, sym
        private filter
        define_method sym do |*args|
          call = { :method => sym, :args => args, :instance => self }
          self.class.__filters__[:before][sym].each do |b|
            send :execute_filter, :before, call, &b
            break if call.has_key?(:return)
          end
          unless call.has_key?(:return)
            call[:return] = send filter, *call[:args] 
            self.class.__filters__[:after][sym].each { |b| send :execute_filter, :after, call, &b }
          end
          call[:return]
        end
      end
    end

    def before_filter (*syms, &block)
      filter(:before, *syms, &block)
    end

    def after_filter (*syms, &block)
      filter(:after, *syms, &block)
    end
  end

  def execute_filter (type, call)
    call[:type] = type
    yield call, *call[:args]
  end

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

Your refactoring





Format Copy from initial code

or Cancel