Динамически создаваемый класс в Ruby - PullRequest
9 голосов
/ 23 февраля 2009

У меня есть класс, который должен выглядеть примерно так:

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

Я хочу иметь возможность определить группу классов, подобных этому, во время выполнения (определить их один раз при запуске), где размер массива и тип, назначенный каждому параметру, определяются во время выполнения из внешнего файла спецификации.

Я вроде как заставил его работать, используя уловки, но это действительно ужасно. Есть ли лучший способ?

Ответы [ 3 ]

32 голосов
/ 23 февраля 2009

Из того, что я понимаю, вам нужно метапрограммирование. Вот фрагмент кода для динамического (на лету) создания классов с помощью метода initialize, который инициализирует переменные экземпляра-

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

Надеюсь, вы сможете настроить его под свои требования.

9 голосов
/ 23 февраля 2009

Во-первых, отчасти причина того, что ваш пример кода не работает для вас, заключается в том, что у вас есть две разные @people переменные - одна - переменная экземпляра , а другая - класс переменная экземпляра .

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

Если вы хотите создать переменные один раз в классе для использования в методах экземпляра этого класса, используйте constants или 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

Несмотря на это, в контексте вашего кода вы, вероятно, не хотите, чтобы все ваши экземпляры Family_Type1 ссылались на одни и те же полицейских и бухгалтеров, верно? Или ты?

Если мы перейдем к использованию переменных класса:

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

Инициализация во время выполнения может быть выполнена с помощью метапрограммирования, как сказал Чирантан, но если вы инициализируете только несколько классов и знаете, как их зовут, вы также можете сделать это, просто используя то, что вы читаете из файла: 1019 *

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
1 голос
/ 23 февраля 2009

Предполагая, что вы хотите создать различных классов для типа / размера массива во время выполнения:

Если (как в Python) класс Ruby определен при выполнении (я думаю, что это так), то вы можете сделать это:

Определите ваш класс внутри функции. Пусть функция получит размер и тип массива в качестве параметров и вернет класс в результате. Таким образом, у вас есть своего рода фабрика классов для вызова каждого определения в вашем spec-файле:)

Если, с другой стороны, вы хотите просто инициализировать @params на основе реальных данных, имейте в виду, что Ruby является динамически типизированным языком: просто переназначьте @params в своем конструкторе на новый массив!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...