Почему включение этого модуля не отменяет динамически сгенерированный метод?

Я пытаюсь переопределить динамически генерируемый метод, включив модуль.

В приведенном ниже примере ассоциация Ripple добавляет метод rows= в таблицу. Я хочу называть этот метод, но потом делать некоторые дополнительные вещи.

Я создал модуль для переопределения метода, считая, что модуль row= сможет вызвать super для использования существующего метода.

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

Однако мой новый rows= никогда не вызывается, о чем свидетельствует тот факт, что если я создаю исключение внутри него, ничего не происходит.

Я знаю, что модуль становится включенным, потому что, если я помещу его в это, мое исключение будет поднято.

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

Кроме того, если вместо rows= модуль определяет somethingelse=, этот метод можно вызвать.

Почему мой метод модуля не переопределяет динамически сгенерированный?

Ответ 1

Сделайте эксперимент:

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

Почему? Ответ прост:

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

B до A в цепочке предков (вы можете думать об этом как B внутри A). Поэтому A.x всегда имеет приоритет над B.x.

Однако это можно обойти:

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"

В ActiveSupport (и, следовательно, Rails) этот шаблон реализован как 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

Обновление. Ruby 2 поставляется с Module # prepend, который переопределяет методы A, делая это alias взломать ненужные для большинства случаев использования.

Ответ 2

Почему мой метод модуля не переопределяет динамически сгенерированный?

Потому что это не так, как работает наследование. Методы, определенные в классе, переопределяют те, которые унаследованы от других классов/модулей, а не наоборот.

В Ruby 2.0 существует Module#prepend, который работает точно так же, как Module#include, за исключением того, что он вставляет модуль в качестве подкласса вместо суперкласса в цепочке наследования.

Ответ 3

Если вы extend экземпляр класса, вы можете это сделать.

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'