Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending a class method in a module

I'm playing with ruby's metaprogramming features, and I'm finding it a bit hairy. I'm trying to wrap a method call using a module. Currently, I'm doing this:

module Bar
  module ClassMethods
    def wrap(method)
      class_eval do
        old_method = "wrapped_#{method}".to_sym
        unless respond_to? old_method
          alias_method old_method, method

          define_method method do |*args|
            send old_method, *args
          end
        end
      end
    end
  end

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

class Foo
  include Bar

  def bar(arg = 'foo')
    puts arg
  end

  wrap :bar
end

Three questions:

  1. Is there any way to do this without renaming the method, so as to allow the use of super? Or something cleaner/shorter?

  2. Is there a clean way to set the default values?

  3. Is there a means to move the wrap :bar call further up?

like image 753
Denis de Bernardy Avatar asked Jul 08 '11 18:07

Denis de Bernardy


1 Answers

1) Cleaner/shorter

module ClassMethods
  def wrap(method)
    old = "_#{method}".to_sym
    alias_method old, method
    define_method method do |*args|
      send(old, *args)
    end
  end
end

class Foo
  extend ClassMethods

  def bar(arg = 'foo')
    puts arg
  end

  wrap :bar
end

As far as I know there is no way to achieve this without renaming. You could try to call super inside the define_method block. But first of all, a call to super from within a define_method will only succeed if you specify arguments explicitly, otherwise you receive an error. But even if you call e.g. super(*args), self in that context would be an instance of Foo. So a call to bar would go to the super classes of Foo, not be found and ultimately result in an error.

2) Yes, like so

define_method method do |def_val='foo', *rest|
  send(old, def_val, *rest)
end

However, in Ruby 1.8 it is not possible to use a block in define_method, but this has been fixed for 1.9. If you are using 1.9, you could also use this

define_method method do |def_val='foo', *rest, &block|
  send(old, def_val, *rest, &block)
end

3) No, unfortunately. alias_method requires the existence of the methods that it takes as input. As Ruby methods come into existence as they are parsed, the wrap call must be placed after the definition of bar otherwise alias_method would raise an exception.

like image 59
emboss Avatar answered Nov 02 '22 08:11

emboss