Reputation: 3618
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
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
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
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