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)
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
included_by_module(klass)
end
end
def define_class_methods(&block)
@class_method_module.module_eval &block
end
end
def mixin_class_methods(&block)
if not (Module === (@class_method_module ||= Module.new))
fail "@class_method_module is not a module!"
end
@extra_include_block = block
extend MixinClassMethods
end
end
It 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.foo
This 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