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_method 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?
In simple words, the difference between include and extend is that 'include' is for adding methods only to an instance of a class and 'extend' is for adding methods to the class but not to its instance.
Class macros are class methods that are only used when a class is defined. They allow us to dry up shared code at across classes. In this post, we'll build a custom class macro that leverages class instance variables to define class-specific attributes.
On the other hand, when we are using the extend keyword in Ruby, we are importing the module code but the methods are imported as class methods. If we try to access the methods that we imported with the instance of the class, the compiler will throw an error.
Macros in Ruby are class methods that generate instance methods. Let us try to understand how macros work by implementing our own version of the has_many macro. has_many declaration is a call to the has_many class method. Upon invocation has_many dynamically generates methods for managing the association.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With