Rails::Generator について

いま Rails::Generator のクラス群を見直していたのだが、自分でGenerator の歩き方などというエントリを書いておきながら早くも理解不能になっていてびびった。ここで、自分自身への備忘の意味でエントリを記す。(自己中なエントリですんません)

Generator はコードが洗練されすぎていて、わけわからん。

Rails::Generator::Scripts::Base#run の

Rails::Generator::Base.instance(options[:generator], args, options).command(options[:command]).invoke!
#($GEMSHOME/rails-1.2.3/lib/rails_generator/scripts.rb)

の一行を理解するのに死ぬほど時間がかかった。

まずは instance メソッドは Rails::Generator::Lookupモジュールの instance メソッドが定義元であるということ。
これが返すインスタンスは options[:generator] で示される Rails::Generator::Base クラスのサブクラスのインスタンスである。

次に command メソッドだが、これはもともとの定義は、Rails::Generator::Commands モジュールの included モジュールメソッドにある。このモジュールは、Rails::Generator::Base クラスに include される。でその定義だが、

def self.included(base)
  base.send(:define_method, :command) do |command|
    Commands.instance(command, self)
  end
end
#($GEMSHOME/rails-1.2.3/lib/rails_generator/commands.rb)

というわけで、動的に include 元のクラスに command という名前のインスタンスメソッドを定義している。Command.instance の定義はそのすぐ上にある。

def self.instance(command, generator)
  const_get(command.to_s.camelize).new(generator)
end
#($GEMSHOME/rails-1.2.3/lib/rails_generator/commands.rb)

const_get(command.to_s.camelize) は command という名前の Rails のクラスオブジェクトを取得している。これは、Rails::Generator::Commands::Base のサブクラスでなければならない。generator には Rails::Generator::Baseクラス(のサブクラス)が来る。

Rails::Generator::Commands::Base の定義の冒頭は次のとおり。

class Base < DelegateClass(Rails::Generator::Base)
...

DelegateClass メソッドを上のように使った場合、Rails::Generator::Commands::Base のコンストラクタには、かならず Rails::Generator::Base のサブクラスのインスタンスを委任(delegate)先のオブジェクトとして指定しなければならない。これがすなわち、

  const_get(command.to_s.camelize).new(generator)

というわけなのだ・・・。ふぎゃー、なんというコードだ。この作者の頭の中身を覗いてみたいわ。

クラス図を描いたほうがよいのだが、要するに自分が定義した Rails::Generator::Base クラスのサブクラスが、ジェネレータの実務が実装されている Rails::Generator::Commands::Base のサブクラスから見て、あたかもスーパークラスのように振舞うようにしたいがために、このような delegate のテクニックを使っている。スーパークラスは動的に変更できないため、それに似た効果を得るためのテクニックといえるだろう。