This question is concerned specifically with adding a class at runtime. The class is to be created within an existing module, but, as will be seen, that is almost incidental and is of questionable utility.
Adding a class to a module at runtime
To assist in the construction of classes within a given module at runtime we might construct a method class_factory.
def class_factory(mod, class_name, consts, meths, instance_meths,
accessors)
class_obj = mod.const_set(class_name, Class.new)
consts.each { |const,val| class_obj.const_set(const,val) }
meths.each do |name,body|
class_obj.singleton_class.
instance_eval("define_method(:#{name}) #{body}")
end
instance_meths.each do |name,body|
class_obj.instance_eval("define_method(:#{name}) #{body}")
end
accessors.each do |accessor,inst_var|
class_obj.public_send(accessor, inst_var)
end
class_obj
end
Let's try it.
module M
end
class_obj = class_factory(
M,
"B",
{ 'A'=>7, 'D'=>'cat' },
{ greeting: '{ |n| "Cat\'s have #{n} lives" }' },
{ initialize: '{ |name| @name = name }',
say_name: '{ "My name is #{@name}" }' },
{ attr_accessor: "name" }
)
#=> M::B
class_obj == M::B
#=> true
M::B.constants
#=> [:A, :D]
class_obj.methods(false)
#=> [:greeting]
M::B.instance_methods(false)
#=> [:say_name, :name=, :name]
class_obj.greeting(9)
#=> "Cat's have 9 lives"
M::B.greeting(5)
#=> "Cat's have 5 lives"
instance = M::B.new "Lola" # or class_obj.new "Lola"
#=> #<M::B:0x000056cb6e766840 @name="Lola">
instance.say_name
#=> "My name is Lola"
instance.name
#=> "Lola"
instance.name = "Lo"
#=> "Lo"
instance.name
#=> "Lo"
It could be that your code may contain static expressions such as these, and the only dynamic part is the construction of the class.
On the other hand, the class may be used dynamically as well. For example:
mod = "M"
cl = "B"
name = "Lola"
meth = "say_name"
Then:
Object.const_get("#{mod}::#{cl}").new(name).public_send(meth)
#=> "My name is Lola"
or
class_obj.new(name).public_send(meth)
#=> "My name is Lola"
How best to reference dynamically-created classes
We have just seen various ways to reference a dynamically-created class. Depending on requirements, M::B versus class_obj and Object.const_get("#{mod}::#{cl}") versus class_obj. Clearly, the use of class_obj is simplest in both cases and has the additional advantage that references to class_obj need not be changed if, in future, M or B in M::B are changed.
Are the advantages to creating classes dynamically that are members of a module?
Recall that the main reason to create a class within a module is create a namespace, so that, for example, M1::C and M2::C do not create a name conflict. However, if we reference a dynamically-created class by its (unique) object (rather than its name, a constant) that is held by a variable (here class_obj) there is no need for the namespace. So the answer to the question I posed in this section is "no".
Moreover, if we reference a dynamically-created class by its object, there is no reason to assign a name to the class. We therefore could modify class_factory as follows:
def class_factory(consts, meths, instance_meths, accessors)
Class.new do
consts.each { |const,val| const_set(const,val) }
meths.each do |name,body|
singleton_class.
instance_eval("define_method(:#{name}) #{body}")
end
instance_meths.each do |name,body|
instance_eval("define_method(:#{name}) #{body}")
end
accessors.each do |accessor,inst_var|
public_send(accessor, inst_var)
end
end
end
class_obj = class_factory(
{ 'A'=>7, 'D'=>'cat' },
{ greeting: '{ |n| "Cat\'s have #{n} lives" }' },
{ initialize: '{ |name| @name = name }',
say_name: '{ "My name is #{@name}" }' },
{ attr_accessor: "name" }
)
#=> #<Class:0x000056cb6eaeefd0>
The object held by class_obj is called an anonymous class because it has no name (that is a constant).
class_obj.constants
#=> [:A, :D]
class_obj.methods(false)
#=> [:greeting]
class_obj.instance_methods(false)
#=> [:say_name, :name=, :name]
instance = class_obj.new "Billy-Bob"
#=> #<#<Class:0x000056cb6eaeefd0>:
# 0x000056cb6eb183d0 @name="Billy-Bob">
instance.say_name
#=> "My name is Billy-Bob"
instance.name
#=> "Billy-Bob"
instance.name = "BB"
#=> "BB"