user3574603
user3574603

Reputation: 3618

Ruby 2.6: How can I dynamically override instance methods when prepending a module?

I have a module called 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

It exposes a class method emit_after. I use it like so:

class Player
  prepend Notifier
  attr_reader :inventory

  emit_after :take

  def take(thing)
    # ...
  end
end

The intention is that by calling emit_after :take, the module overrides #take with its own method.

But the instance method isn't being overridden.

I can however, override it explicitly without using 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...

I know that ClassMethods#emit_after is called so I assume that the method is being defined but it just never gets called.

I want to create the methods dynamically. How can I ensure that the generate method overrides my instance method?

Upvotes: 4

Views: 2078

Answers (3)

chumakoff
chumakoff

Reputation: 7024

@Konstantin Strukov's solution is good but maybe a little confusing. So, I suggest another solution, which is more like the original one.

Your first goal is to add a class method (emit_after) to your class. To do that you should use extend method without any hooks such as self.prepended(), self.included() or self.extended().

prepend, as well as include, are used to add or override instance methods. But that is your second goal and it happens when you call emit_after. So you shouldn't use prepend or include when extending your class.

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

Now it is obvious that you call extend Notifier in order to add the emit_after class method and all the magic is hidden in the method.

Upvotes: 7

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Prepend to the currently opened class:

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

Upvotes: 1

Konstantin Strukov
Konstantin Strukov

Reputation: 3019

What about this solution:

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

It's quite similar to the solution from Aleksei Matiushkin, but the ancestors' chain is a bit cleaner (no "useless" Notifier there)

Upvotes: 3

Related Questions