tags:

views:

956

answers:

3

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?

+1  A: 

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!

Daren Thomas
Thanks - got it so far with creating inside a function, but the question is regarding the implementation - e.g. adding/initializing the array outside of the 'initialize' method etc.
In initialize I only want to send the value - not complete objects.Lets say the instance variable is an array of Person derived objects, then I want the array with the types to be part of the class definition, and in initialize I will only pass the names of each person.
+5  A: 

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.

Chirantan
Thanks, seems more tricky when the instance variable is an array of objects, and the initialize method should only set a certain field in each object.
Dude, in your case you can do instance_variable_set("@params[#{i}].age", ages[i]) and you are done... Thats how you can deal with arrays. You need to dig into it a little to understand it.
Chirantan
+2  A: 

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"
  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
rampion
This is the only answer that really explains the whole situation IMO.
Chuck