Jimmy
Jimmy

Reputation:

Dynamically creating class in Ruby

I have a class that should look something like this:

class Family_Type1
    @people = Array.new(3)
    @people[0] = Policeman.new('Peter', 0)
    @people[1] = Accountant.new('Paul', 0)
    @people[2] = Policeman.new('Mary', 0)

    def initialize(*ages)
        for i in 0 ... @people.length
            @people[i].age = ages[i]
        end
    end
end

I want to be able to define a bunch of classes similar to this one at runtime (define them once at startup) where the size of the array and the type assigned to each parameter is defined at runtime from an external specification file.

I sort of got it to work using evals but this is really ugly. Any better way?

Upvotes: 9

Views: 12936

Answers (3)

rampion
rampion

Reputation: 89133

First off, part of the reason your example code isn't working for you is that you have two different @people variables - one is an instance variable and the other is a class instance variable.

class Example
  # we're in the context of the Example class, so 
  # instance variables used here belong to the actual class object,
  # not instances of that class
  self.class #=> Class
  self == Example #=> true
  @iv = "I'm a class instance variable"

  def initialize
    # within instance methods, we're in the context
    # of an _instance_ of the Example class, so
    # instance variables used here belong to that instance.
    self.class #=> Example
    self == Example #=> false
    @iv = "I'm an instance variable"
  end
  def iv
    # another instance method uses the context of the instance
    @iv #=> "I'm an instance variable"
  end
  def self.iv
    # a class method, uses the context of the class
    @iv #=> "I'm a class instance variable"
  end
end

If you want to create variables one time in a class to use in instance methods of that class, use constants or class variables.

class Example
  # ruby constants start with a capital letter.  Ruby prints warnings if you
  # try to assign a different object to an already-defined constant
  CONSTANT_VARIABLE = "i'm a constant"
  # though it's legit to modify the current object
  CONSTANT_VARIABLE.capitalize!
  CONSTANT_VARIABLE #=> "I'm a constant"

  # class variables start with a @@
  @@class_variable = "I'm a class variable"

  def c_and_c
    [ @@class_variable, CONSTANT_VARIABLE ] #=> [ "I'm a class variable", "I'm a constant" ]
  end
end

Even so, in the context of your code, you probably don't want all your instances of Family_Type1 to refer to the same Policemen and Accountants right? Or do you?

If we switch to using class variables:

class Family_Type1
    # since we're initializing @@people one time, that means
    # all the Family_Type1 objects will share the same people
    @@people = [ Policeman.new('Peter', 0), Accountant.new('Paul', 0), Policeman.new('Mary', 0) ]

    def initialize(*ages)
        @@people.zip(ages).each { |person, age| person.age = age }
    end
    # just an accessor method
    def [](person_index)
      @@people[person_index]
    end
end
fam = Family_Type1.new( 12, 13, 14 )
fam[0].age == 12 #=> true
# this can lead to unexpected side-effects 
fam2 = Family_Type1.new( 31, 32, 29 )
fam[0].age == 12 #=> false
fam2[0].age == 31 #=> true
fam[0].age == 31 #=> true

The runtime initialization can be done with metaprogramming, as Chirantan said, but if you are only initializing a few classes, and you know what their name is, you can also do it just by using whatever you read from the file:

PARAMS = File.read('params.csv').split("\n").map { |line| line.split(',') }
make_people = proc do |klasses, params|
  klasses.zip(params).map { |klass,name| klass.new(name, 0) }
end
class Example0
  @@people = make_people([ Fireman, Accountant, Fireman ], PARAMS[0])
end
class Example1
  @@people = make_people([ Butcher, Baker, Candlestickmaker ], PARAMS[0])
end

Upvotes: 9

Chirantan
Chirantan

Reputation: 15664

From what I understand, you need meta-programming. Here is a snippet of code for creating classes dynamically (on the fly) with initialize method that initializes instance variables-

class_name = 'foo'.capitalize
klass = Object.const_set(class_name,Class.new)

names = ['instance1', 'instance2'] # Array of instance vars

klass.class_eval do
  attr_accessor *names

  define_method(:initialize) do |*values|
    names.each_with_index do |name,i|
      instance_variable_set("@"+name, values[i])
    end
  end
  # more...
end

Hope you can tweak it to suit your requirements.

Upvotes: 34

Daren Thomas
Daren Thomas

Reputation: 70354

Assuming you want to create different classes per type/array size at runtime:

If (like in Python) a Ruby class is defined when executed (I think it is), then you can do this:

Define your class inside a function. Have the function recieve array size and type as parameters and return the class in its result. That way, you have a sort of class factory to call for each definition in your spec file :)

If on the other hand you want to just initialize @params based on actual data, keep in mind, that Ruby is a dynamically typed language: Just reassign @params in your constructor to the new array!

Upvotes: 1

Related Questions