scramble cadenza

技術ネタのガラクタ置き場

ruby で動的にクラスを作る

結論

  1. 定数に class クラスのオブジェクトを代入すると、(ruby で一般的な) class として扱われる
  2. module or class の配下にネストして class を作る場合、その class が持つ定数として定義する
  3. class.new する際に引数として、class クラスのオブジェクトを渡すと、引数で渡されたクラスで継承されたクラスが生成される。
  4. メソッドも作りたい場合はブロックで記述する

主な使いどころとしては、文字列で与えたクラス名で、クラスを新たに作成するとき。

実演

何言ってるのかわからないので確かめてみる

クラスを作る

結論1: 定数にclassクラスのオブジェクトを代入すると、(ruby で一般的な) class として扱われる

  • module#const_set メソッドを使う
    • 第一引数に定数名、第二引数にセットしたいオブジェクトを渡す
self.class.const_set :'Creature', Class.new
=> Creature
Creature
=> Creature

以下と同じ

class Creature
end

もちろんクラス名を Array に入れて each すると

['Human', 'Birds', 'Fishes'].each do |species|
  self.class.const_set :"#{species}", Class.new
end

人類、魚類、鳥類が作成できる。 以下に紹介する方法もこの方法が応用できる。

ネストしたクラスを作る

結論2: module or class の配下にネストして class を作る場合、その class が持つ定数として定義する

  • 以下の場合、const_set メソッドの self は Creature という class クラスのオブジェクト
    • Creature クラスに、Human という定数を定義している
class Creature
  const_set :'Human', Class.new
end
=> Creature::Human

以下と同じ

class Creature
  class Human
  end
end

クラスを継承させる

結論3: class.new する際に引数として、class オブジェクトを渡すと、継承される

  • 以下の例では、Creature::Base を継承させて Creature::Human を作っている
  • Class#new を使うだけ
class Creature
  const_set :'Base', Class.new
  const_set :'Human', Class.new(Base)
end

Creature::Human.superclass
=> Creature::Base

つまり

class Creature
  class Base
  end

  class Human < Base
  end
end

と同じ

メソッドも作りたい場合

結論4: メソッドも作りたい場合はブロックで記述する

  • Creature::Base クラスに eat メソッド、Creature::Human クラスに think メソッドを定義している
    • Class#new にブロックを渡して、def メソッドで定義する
    • さらに Base を継承して Human を作っている
  • 複数行にわたってブロックを書く場合、const_set の引数渡しには ( ) が必要なので注意
    • これで少しハマッた
class Creature
  const_set(:'Base', Class.new do |klass|
    def eat(food)
      p "#{food} mgmg..."
    end
  end)

  const_set(:'Human', Class.new(Base) do |klass|
    def think
      p 'I think, therefore I am'
    end
  end)
end
=> Creature::Human

h = Creature::Human.new
=> #<Creature::Human:0x007fa324b6a7a0>
h.think
=> "I think, therefore I am"
h.eat('rice')
=> "rice mgmg..."

もちろん以下とおなじ

class Creature
  class Base
    def eat(food)
      p "#{food} mgmg..."
    end
  end

  class Human < Base
    def think
      p 'I think, therefore I am'
    end
  end
end

おまけ

  • ruby が単一継承だったり
  • class 名が先頭大文字始まりだったり
  • クラスを二度定義しようとすると、"warning: already initialized constant" が出たり
  • class 定義がブロックっぽい書き方だったり

の理由が理解できました。