Ruby 2.6: Как я могу динамически переопределить методы экземпляра при добавлении модуля? - PullRequest
2 голосов
/ 19 марта 2019

У меня есть модуль с именем Notifier.

module Notifier
  def self.prepended(host_class)
    host_class.extend(ClassMethods)
  end

  module ClassMethods
    def emit_after(*methods)
      methods.each do |method|
        define_method(method) do |thing, block|
          r = super(thing)
          block.call
          r
        end
      end
    end
  end
end

Он предоставляет метод класса emit_after. Я использую это так:

class Player
  prepend Notifier
  attr_reader :inventory

  emit_after :take

  def take(thing)
    # ...
  end
end

Предполагается, что при вызове emit_after :take модуль переопределяет #take своим собственным методом.

Но метод экземпляра не переопределяется.

I может однако, переопределить его явно, не используя ClassMethods

module Notifier
  def self.prepended(host_class)
    define_method(:take) do |thing, block|
      r = super(thing)
      block.call
      r
    end
  end

class Player
  prepend Notifier
  attr_reader :inventory

  def take(thing)
    # ...
  end
end

#> @player.take @apple, -> { puts "Taking apple" }
#Taking apple
#=> #<Inventory:0x00007fe35f608a98...

Я знаю, что ClassMethods#emit_after вызывается, поэтому я предполагаю, что метод определяется, но он никогда не вызывается.

Я хочу создавать методы динамически. Как я могу гарантировать, что метод generate переопределит мой метод экземпляра?

Ответы [ 3 ]

2 голосов
/ 19 марта 2019

Как насчет этого решения:

module Notifier
  def self.[](*methods)
    Module.new do
      methods.each do |method|
        define_method(method) do |thing, &block|
          super(thing)
          block.call if block
        end
      end
    end
  end
end

class Player
  prepend Notifier[:take]

  def take(thing)
    puts "I'm explicitly defined"
  end
end

Player.new.take(:foo) { puts "I'm magically prepended" }
# => I'm explicitly defined
# => I'm magically prepended

Это очень похоже на решение от Алексея Матюшкина, но цепь предков немного чище (там нет "бесполезного" уведомителя)

1 голос
/ 19 марта 2019

@Konstantin Strukov решение хорошее, но, возможно, немного запутанное. Итак, я предлагаю другое решение, которое больше похоже на оригинальное.

Ваша первая цель - добавить метод класса (emit_after) к вашему классу. Для этого вы должны использовать метод extend без каких-либо хуков, таких как self.prepended(), self.included() или self.extended().

prepend, а также include, используются для добавления или переопределения методов экземпляра . Но это ваша вторая цель, и это происходит, когда вы звоните emit_after. Поэтому вы не должны использовать prepend или include при расширении класса.

module Notifier
  def emit_after(*methods)
    prepend(Module.new do
      methods.each do |method|
        define_method(method) do |thing, &block|
          super(thing)
          block.call if block
        end
      end
    end)
  end
end

class Player
  extend Notifier

  emit_after :take

  def take(thing)
    puts thing
  end
end

Player.new.take("foo") { puts "bar" }  
# foo
# bar
# => nil

Теперь очевидно, что вы вызываете extend Notifier, чтобы добавить метод класса emit_after, и вся магия скрыта в нем.

1 голос
/ 19 марта 2019

Добавить к текущему открытому классу:

module Notifier
  def self.prepended(host_class)
    host_class.extend(ClassMethods)
  end

  module ClassMethods
    def emit_after(*methods)
    # ⇓⇓⇓⇓⇓⇓⇓ HERE  
      prepend(Module.new do
        methods.each do |method|
          define_method(method) do |thing, block = nil|
            super(thing).tap { block.() if block }
          end
        end
      end)
    end
  end
end

class Player
  prepend Notifier
  attr_reader :inventory

  emit_after :take

  def take(thing)
    puts "foo"
  end
end

Player.new.take :foo, -> { puts "Taking apple" }
#⇒ foo
#  Taking apple
...