How do you extend a Ruby module with macro-like metaprogramming methods?
Consider the following extension (the pattern popularized by several Rails plugins over the years):
module Extension
def self.included(recipient)
recipient.extend ClassMethods
recipient.send :include, InstanceMethods
end
module ClassMethods
def macro_method
puts "Called macro_meth开发者_Python百科od within #{self.name}"
end
end
module InstanceMethods
def instance_method
puts "Called instance_method within #{self.object_id}"
end
end
end
If you wished to expose this to every class, you can do the following:
Object.send :include, Extension
Now you can define any class and use the macro method:
class FooClass
macro_method
end
#=> Called macro_method within FooClass
And instances can use the instance methods:
FooClass.new.instance_method
#=> Called instance_method within 2148182320
But even though Module.is_a?(Object)
, you cannot use the macro method in a module.
module FooModule
macro_method
end
#=> undefined local variable or method `macro_method' for FooModule:Module (NameError)
This is true even if you explicitly include the original Extension
into Module
with Module.send(:include, Extension)
.
For individual modules you can include extensions by hand and get the same effect:
module FooModule
include Extension
macro_method
end
#=> Called macro_method within FooModule
But how do you add macro-like methods to all Ruby modules?
Consider the following extension (the pattern popularized by several Rails plugins over the years)
This is not a pattern, and it was not "popularized". It is an anti-pattern that was cargo-culted by 1337 PHP h4X0rZ who don't know Ruby. Thankfully, many (all?) instances of this anti-pattern have been eliminated from Rails 3, thanks to the hard word of Yehuda Katz, Carl Lerche and the others. Yehuda even uses pretty much the exact same code you posted as an anti-example both in his recent talks about cleaning up the Rails codebase, and he wrote an entire blog post just about this one anti-pattern.
If you wished to expose this to every class, you can do the following:
Object.send :include, Extension
If you want to add it to Object
anyway, then why not just do that:
class Object
def instance_method
puts "Called instance_method within #{inspect}"
end
end
But how do you add macro-like methods to all Ruby modules?
Simple: by adding them to Module
:
class Module
def macro_method
puts "Called macro_method within #{inspect}"
end
end
It all just works:
class FooClass
macro_method
end
#=> Called macro_method within FooClass
FooClass.new.instance_method
#=> Called instance_method within #<FooClass:0x192abe0>
module FooModule
macro_method
end
#=> Called macro_method within FooModule
It's just 10 lines of code vs. your 16, and exactly 0 of those 10 lines are metaprogramming or hooks or anything even remotely complicated.
The only difference between your code and mine is that in your code, the mixins show up in the inheritance hierarchy, so it is a tad easier to debug, because you actually see that something was added to Object
. But that is easily fixed:
module ObjectExtensions
def instance_method
puts "Called instance_method within #{inspect}"
end
end
class Object
include ObjectExtensions
end
module ModuleExtensions
def macro_method
puts "Called macro_method within #{inspect}"
end
end
class Module
include ModuleExtensions
end
Now I'm tied with your code at 16 lines but I would argue that mine is simpler than yours, especially considering that yours doesn't work and neither you nor I nor almost 190000 StackOverflow users can figure out why.
精彩评论