Add class to ruby module in runtime

Viewed 360

I need to add class into the class (or module) in runtime. For example: I have Module M, Class A. I need to add a new Class B to Module M. I've tried to do:

M.module_eval do
  class B
  end
end

But it doesnt work.

I know that (for example) if I want to add a method to existing class A, I would do

class << A
def method
end
end

But how to add a Class to a Module or Class?

1 Answers

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" 
Related