Why does including this module not override a dynamically-generated method?

雨燕双飞 提交于 2019-12-04 23:45:10

问题


I'm trying to override a dynamically-generated method by including a module.

In the example below, a Ripple association adds a rows= method to Table. I want to call that method, but also do some additional stuff afterwards.

I created a module to override the method, thinking that the module's row= would be able to call super to use the existing method.

class Table

  # Ripple association - creates rows= method
  many :rows, :class_name => Table::Row

  # Hacky first attempt to use the dynamically-created
  # method and also do additional stuff - I would actually
  # move this code elsewhere if it worked
  module RowNormalizer
    def rows=(*args)
      rows = super
      rows.map!(&:normalize_prior_year)
    end
  end
  include RowNormalizer

end

However, my new rows= is never called, as evidenced by the fact that if I raise an exception inside it, nothing happens.

I know the module is getting included, because if I put this in it, my exception gets raised.

      included do
        raise 'I got included, woo!'
      end

Also, if instead of rows=, the module defines somethingelse=, that method is callable.

Why isn't my module method overriding the dynamically-generated one?


回答1:


Let's do an experiment:

class A; def x; 'hi' end end
module B; def x; super + ' john' end end
A.class_eval { include B }

A.new.x
=> "hi" # oops

Why is that? The answer is simple:

A.ancestors
=> [A, B, Object, Kernel, BasicObject]

B is before A in the ancestors chain (you can think of this as B being inside A). Therefore A.x always takes priority over B.x.

However, this can be worked around:

class A
  def x
    'hi'
  end
end

module B
  # Define a method with a different name
  def x_after
    x_before + ' john'
  end

  # And set up aliases on the inclusion :)
  # We can use `alias new_name old_name`
  def self.included(klass)
    klass.class_eval {
      alias :x_before :x 
      alias :x :x_after
    }
  end
end

A.class_eval { include B }

A.new.x #=> "hi john"

With ActiveSupport (and therefore Rails) you have this pattern implemented as alias_method_chain(target, feature) http://apidock.com/rails/Module/alias_method_chain:

module B
  def self.included(base)
    base.alias_method_chain :x, :feature
  end

  def x_with_feature
    x_without_feature + " John"
  end
end

Update Ruby 2 comes with Module#prepend, which does override the methods of A, making this alias hack unnecessary for most use cases.




回答2:


Why isn't my module method overriding the dynamically-generated one?

Because that's not how inheritance works. Methods defined in a class override the ones inherited from other classes/modules, not the other way around.

In Ruby 2.0, there's Module#prepend, which works just like Module#include, except it inserts the module as a subclass instead of a superclass in the inheritance chain.




回答3:


If you extend the instance of the class, you will can do it.

class A
  def initialize
    extend(B)
  end
  def hi
    'hi'
  end
end
module B
  def hi
    super[0,1] + 'ello'
  end
end

obj = A.new
obj.hi #=> 'hello'


来源:https://stackoverflow.com/questions/4741373/why-does-including-this-module-not-override-a-dynamically-generated-method

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!