Mixing in Class Methods
It is pretty common to have some conceptual block of functionality that is appropriate for a module, except it involves a combination of instance methods and class methods. The issue of singleton methods on a module not being added as singleton methods on a class when mixing in the module has come up repeatedly on the mailing list. While it is a compelling argument that wanting to mix in class methods as well as instance methods is the common case, having that as the only behavior prevents some desirable flexibility (particularly involving DSLs).
Instead, consider the following idiom (Updated! thanks, Sas):
module M module ClassMethods; end def self.included(klass) klass.extend(ClassMethods) end def foo puts "Calling class bar() method from foo()" self.class.bar end module ClassMethods def bar puts "Called #{self}::bar()" end end end
We can now define methods intended to be mixed into the including class in the M::ClassMethods module. This is a naïve solution, however, and does not handle the situation in which a module is included in another module. Unfortunately, the simplicity of a ClassMethods module can't deal with including one module in another since the same ClassMethods module would be shared across both modules. In addition, we might like to provide some code to be called when our module is included, but the Module#included is what we use to make the class method mixin work. To make this easier, let's push the functionality into the Module class (which is the superclass of all modules just as Class is the superclass of all classes):
class Module private module MixinClassMethods def included_by_module(klass) #check to see if klass is already set up if not klass.instance_variables.include? '@class_method_module' klass.send(:mixin_class_methods) end klass_method_module = klass.instance_variable_get('@class_method_module') klass_method_module.send(:include, @class_method_module) end def included(klass) @extra_include_block.call(klass) if @extra_include_block case klass when Class klass.extend(@class_method_module) when Module #more work to include in a module included_by_module(klass) end end def define_class_methods(&block) @class_method_module.module_eval &block end end def mixin_class_methods(&block) #ensure the existence of the ClassMethods module if not (Module === (@class_method_module ||= Module.new)) fail "@class_method_module is not a module!" end @extra_include_block = block extend MixinClassMethods end endIt is now possible to do the following:
require 'mixin_class_methods' module M mixin_class_methods { |klass| puts "Module M has been included by #{klass}" } def foo puts "Calling class bar() method from foo()" self.class.bar end define_class_methods { def bar puts "Called #{self}::bar()" end } end module N include M end class Baz include N end Baz.bar Baz.new.fooThis will produce the following output:
Module M has been included by N Called Baz::bar() Calling class bar() method from foo() Called Baz::bar()Enjoy!
Labels: Metaprogramming, Ruby
3 Comments:
At 7/04/2006 01:30:00 PM, Anonymous said…
Nice blog, Gregory
But I got stuck right at the begining
I have a file test.rb with the following
--------------
module M
module ClassMethods; end
def included(klass)
klass.extend(ClassMethods)
end
def foo
puts "Calling class bar() method from foo()"
self.class.bar
end
module ClassMethods
def bar
puts "Called #{self}::bar()"
end
end
end
class C
include M
end
c = C.new
c.foo
-------------
when I execute I get the following
>ruby C:\temp\ruby\test2.rb
Calling class bar() method from foo()
C:/temp/ruby/test2.rb:9:in `foo': undefined method `bar' for C:Class (NoMethodError)
from C:/temp/ruby/test2.rb:24
>exit
I guess I must be doing something wrong...
At 7/04/2006 02:17:00 PM, Unknown said…
I've found the problem
where it says
---
def included(klass)
klass.extend(ClassMethods)
end
---
should say
---
def M.included(klass)
or even better
def self.included(klass)
---
Saludos
Sas
At 12/12/2007 02:29:00 PM, Anonymous said…
module Base
def self.included( dest_module )
super
def dest_module.module_name
self.name
end
end
end
module User
include Base
end
>> User.module_name
=> "User"
>> User.singleton_methods
=> ["module_name"]
One thing I am struggling with is how do document the likes of module_name
Post a Comment
<< Home