Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby method_added callback not trigger including Modules

I wanted to write a little "Deprecate-It" lib and used the "method_added" callback a lot. But now I noticed that this callback is not triggered, when including a module.

Are there any callbacks or workarounds, to get class "Foobar" informed when somewhing is included to itself?

Small Demo to demonstrate:

# Including Moduls won't trigger method_added callback

module InvisibleMethod
  def invisible
    "You won't get a callback from me"
  end
end

class Foobar
  def self.method_added(m)
    puts "InstanceMethod: '#{m}' added to '#{self}'"
  end

  def visible
    "You will get a callback from me"
  end

  include InvisibleMethod
end

[:invisible, :visible, :wont_exist].each do |meth|
  puts "#{meth}: #{Foobar.public_method_defined? meth}"
end

That's the result:

InstanceMethod: 'visible' added to 'Foobar'
invisible: true
visible: true
wont_exist: false

Additional Information:

I really need to use a hook like method_added.

ActiveModel is adding public_instance_methods to Class during runtime though anonymous Modules.

like image 717
Deradon Avatar asked Feb 21 '12 22:02

Deradon


3 Answers

The problem is that including modules doesn't add methods to the classes - it only changes the method call chain. This chain defines which classes/module will be searched for a method, that is not defined for the class in question. What happens when you include a module is an addition of an entry in that chain.

This is exactly the same as when you add a method in a superclass - this doesn't call method_added since it is not defined in the superclass. It would be very strange if a subclass could change the behavior of a superclass.

You could fix that by manually calling method added for an included module, by redefining include for your class:

class Foobar
  def self.include(included_module)
    included_module.instance_methods.each{|m| self.method_added(m)}
    super
  end
end

And it is much safer than redefining the included method in Module - the change is narrowed only to the classes, that you have defined yourself.

like image 146
Aleksander Pohl Avatar answered Nov 15 '22 02:11

Aleksander Pohl


As suggested by one of the comments, you could use some other hook to get the behavior you want. For example, try to add this at the beginning of your code:

class Module
  def included(klass)
    if klass.respond_to?(:method_added)
      self.instance_methods.each do |method|
        klass.method_added(method)
      end
    end
  end
end

Whenever a module is included in a class, all instance methods of that module will be notified to the class, as long as it defines the method method_added. By running your code with the change above I get this result:

InstanceMethod: 'visible' added to 'Foobar'
InstanceMethod: 'invisible' added to 'Foobar'
invisible: true
visible: true
wont_exist: false

Which I think is the behavior you want.

like image 32
Adiel Mittmann Avatar answered Nov 15 '22 02:11

Adiel Mittmann


I think deprecation is not big isue to require a library. it is implemented like this in datamapper. About method_added hook; it is working as expected because methods already added to module not class. Only you can get your expected result monkey patching included hook.

# got from https://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/deprecate.rb
module Deprecate
  def deprecate(old_method, new_method)
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{old_method}(*args, &block)
        warn "\#{self.class}##{old_method} is deprecated, use \#{self.class}##{new_method} instead (\#{caller.first})"
        send(#{new_method.inspect}, *args, &block)
      end
    RUBY
  end
end # module Deprecate

class MyClass
  extend Deprecate

  def old_method
    p "I am old"
  end
  deprecate :old_method, :new_method

  def new_method
    p "I am new"
  end
end

m = MyClass.new
m.old_method

# MyClass#old_method is deprecated, use MyClass#new_method instead (pinger.rb:27:in `<main>')
# "I am new"
like image 45
Selman Ulug Avatar answered Nov 15 '22 04:11

Selman Ulug