読者です 読者をやめる 読者になる 読者になる

Rails::Initializerを読む。

rails RailsHackingGuide

目次はこちらです。

まずは、rails-stable/railties/lib/initializer.rbから読みます。このファイルではRails全体の初期化を行なっています。

File.join, File.dirname, __FILE__

require File.join(File.dirname(__FILE__), 'railties_path')

Rubyのライブラリではよく出てくる表現なので一応解説しておく。

File.joinはディレクトリ区切り文字で引数を連結した文字列を返す。この場合、Unixであれば"#{File.dirname(__FILE__)}/railties_path"、Windowsであれば"#{File.dirname(__FILE__)}\railties_path"を返す。

File.dirname(filename)はfilenameの一番後ろのスラッシュより前の文字列を返す。もしスラッシュがなければカレントディレクトリ(つまり".")を返します。

__FILE__はRubyの組み込み変数で現在のソースファイル名です。似たようなので$0というのもありますがこちらは実行中のスクリプトファイルを表します。Rubyではよく出てくるので違いをしっかり覚えておきましょう。

つまり、ここのコードでは、このファイル(initializer.rb)と同じディレクトリにあるrailties_path.rbをrequireしています。

RAILS_ENVの設定

RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defines?(RAILS_ENV)

定数RAILS_ENVが定義されていなければ定義する。環境変数RAILS_ENVがセットされていればそれを用い、それがなければdevelopmentをセットする。

Object#dupを使って複製しているのがちょっとよく分からない。複製していないとRails起動中にENV['RAILS_ENV']が代えられてしまった場合、内部動作に整合性が保てなくなってしまうからだろうか。なかなかどういうときにdupすればよいかというのは難しそうだ。というよりうっかり書き忘れてしまいそうだ。

Rails::Initialize.run

def self.run(command = :process, configuration = Configuration.new)
  yield configuration if block_given?
  initializer = new configuration
  initializer.send(command)
  initializer
end

ブロックでRailsの設定が出来ます。ConfigurationクラスはRailsの設定に関わるクラスです。Railsアプリケーションの作成時にはconfig/environment.rbでこれらの設定を行います。

この中の

  initializer = new configuration

というのがビックリする。Javaっぽいけどこれはどういうことなのだろうか?

クラスメソッドの中なので、同じクラスメソッドのnewのレシーバが省略できるらしい。

つまり、

  initializer = self.new configuration

ということのようだ。

ここでInitializerにせずselfにしたのはひょっとするとクラス名を変更したい、あるいはaliasしたいといった場合があるからである。クラス定義文の中ではなるべくクラス名は書かずにselfとしておいた方が継承したときなどにいろいろ厄介ごとがなくて済む。

個人的にはどこで定義されているのか混乱してしまうのでクラスメソッドのレシーバ省略はあまりしたくないのですが、省略する方が一般的なのでしょうか?やっぱり派閥に分かれているのかも。まぁできるだけ省略したくないと思うのはそれだけ使い込んでないということなんだろうと思う。今回すぐにクラスメソッドの省略が見抜けなかったのも経験不足ゆえだろう。

次の行はInitializerの任意のインスタンスメソッドをObject#sendを使って実行させている。デフォルトではprocessメソッドが実行されるらしい。

確か1.8以降だとObject#__send__を使わないと警告が出るんだったような気がする。ここらあたりはどうやって回避しているのだろうか。もしかすると再定義されたメソッドなのかもしれない。自分で探してみた感じどうもそれっぽいのが分からないのですがどうなんでしょうか?

Configurationクラス

ここで先にConfigurationクラスを呼んだ方が理解しやすいので一旦Configurationクラスの基本的なところを押さえておく。

コンストラクタ

アクセサにズラーと初期値を代入している。初期値は"default_" + アクセサ名のメソッドで定義してある。

以下気になったところのみ定義されている順番にピックアップ。

default_load_paths

500行目:

 paths = ["#{root_path}/test/mocks/#{environment}"]

カッコが省略されているのでわかりづらいがそれ以前にroot_pathやenvironmentといったローカル変数が定義されていないのでこれはメソッドだ。ということで探してみると案の定あった。

def root_path
  ::RAILS_ROOT
end

def environment
  ::RAILS_ENV
end

定数を参照しているだけなのにメソッドを作ってるのは何故?

RAILS_ROOT = ::RAILS_ROOT

とかだといけないのか?

503行目:

paths.concat(Dir["#{root_path}/app/models/[_a-z]*"])

書き方からしてDir.glob()のaliasのようだなと思って探してみたらRuby標準で定義されてた。ActiveSupport拡張だと思ってた。今まで知らずに全部Dir.globとか書いてたよ…。ワンライナーとか全部Dirに変更だな。

というより考えてみたら当たり前なのだけど、クラスメソッドに(args)といったメソッドを定義すれば組み込みメソッドみたいに使えるのね。最初はDirといった定数にハッシュを入れておいてそれにアクセスだとか、Kernel#Dir(array)といったメソッドを定義しておいてそれのカッコの省略なんじゃないかと疑った。

default_log_level

log_levelはinfo、debugという2種類のモードがあるようだ。development, testのときはdebugモードで作動する。

default_controller_paths

controllerは"#{root_path}/app/controllers/"だけではなく、"#{root_path}/components/"や"#{RAILTIES_PATH}/builtin/controllers/" に置くことが出来る。

RAILTIES_PATHはrailties_path.rbで定義されている。
railties_path.rb:

RAILTIES_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..'))

File.expand_pathは相対パスの文字列を絶対パスへ展開する。その際、第2引数を基準にする。第2引数が無ければカレントディレクトリとみなす。~の展開もしてくれるらしい。

つまり、Railsライブラリの置いてあるディレクトリのbuiltin/controllers/にコントローラを置くことも出来る。

普通は"app/controllers"を使うけど、"components/"とbuiltin/controllers/はどう使い分けるのだろうか?



他のメソッドは今はちょっとよく分からないので置いておく。

コンストラクタの最後では、フレームワークそれぞれのアクセサメソッドにOrderedOptionsクラスのインスタンスを渡している。

database_configuration

少し読みづらいので改行した。

def database_configuration
  YAML::load(
    ERB.new(
      IO.read(database_configuration_file)
    ).result)
end

"config/database.yml"からYAMLファイルを読んできてRubyのデータ構造へ戻す。興味深いのはあいだにERBを挟むことでYAMLファイル内でRubyコードを入れることが出来るようにしている点である。

ERB#resultはERBのコード部分を展開して文字列として返す。

OrderedOptions, OrderedHash

OrderedOptions、OrderedHashは実はActiveSupportでも定義されている。ActiveSupportをrequireする前に必要であるためにDRYじゃないがこちらでも定義せざるを得なかったようだ。OrderedOptionsやOrderedHashを別ライブラリにするという手もあったのではないかと思ってみる。

そうしてOrderedOptionsクラスを見てみると、OrderedHashクラスを継承している。ということでOrderedHashクラスを先に読む。

OrderedHashクラスはArrayを継承している。名前どおり順番を保持したHashを実装している。実装は特に難がなさそうなので飛ばす。順序つき配列を使いたいケースは結構あるので一応こんなのがあったと頭にとめておくと参考に出来る。

ということでOrderedOptionsを読む。

ここで注意することは、OrderedOptionsがキーとして文字列を取るのに対し、OrderedHashはキーとしてシンボルをとる。ActiveSupportをまだrequireしていないのでシンボル・文字列間の自動変換はやってくれない。

method_misingを用いてキーのアクセサっぽくアクセスできるようにしている。

  def method_missing(name, *args)
    if name.to_s =~ /(.*)=$/
      self[$1.to_sym] = args.first
    else    
      self[name]
    end
  end

このテクニックは色んなところで使えそうかも。

Initializer#process

ようやくInitializerクラスへ戻ってこれた。次は、Initializer#processを読む。

    def process
      check_ruby_version
      set_load_path
      set_connection_adapters

      require_frameworks
      load_environment

      initialize_database
      initialize_logger
      initialize_framework_logging
      initialize_framework_views
      initialize_dependency_mechanism
      initialize_breakpoints
      initialize_whiny_nils
      initialize_temporary_directories

      initialize_framework_settings
      
      load_environment
      
      add_support_load_paths

      load_plugins

      initialize_routing
      after_initialize
    end

メソッド名が分かりやすいので何の処理を行なっているかが読んだだけで大体は分かる。分かりにくいところ・気になるところだけ以下補足。

check_ruby_version, ruby_version_check.rb

check_ruby_versionはruby_version_check.rbをrequireしているだけなので、ruby_version_check.rbをみてみる。

どうもRubyの1.8.2と1.8.4以降ではRailsは動作して、1.8.3は動かないらしい。どうしてかよく分からない。安定版と開発版が交互だったのだろうか。でもここ最近は1.9.1が開発版だし、1.8.3がそんな前とも思えない。

load_environment
    def load_environment
      silence_warnings do
        config = configuration
        constants = self.class.constants
        eval(IO.read(configuration.environment_path), binding)
        (self.class.constants - constants).each do |const|
          Object.const_set(const, self.class.const_get(const))
        end
      end
    end

silence_warningsというメソッドが定義されている。名前だけでも十分機能を想像できるが一応読んでおく。

silence_warningsはactivesupport/core_ext/kernel/reporting.rbで定義されている。

  def silence_warnings
    old_verbose, $VERBOSE = $VERBOSE, nil
    yield   
  ensure  
    $VERBOSE = old_verbose
  end

多重代入を用いて$VERBOSEを一旦old_verboseに非難しておいて$VERBOSEにnilを代入する。今まであまり多重代入は使ってこなかったけど、こういうケースだと意味も明確になるので良さそうな気がする。

このメソッドの難点は警告が使っている間全く表示されないことだろう。Rubyのバージョンが変わって警告が出るようになっても気づかない恐れもある。警告は煩わしいが、出すには出すなりの理由があると思う。

例えば、先ほども出てきたObject#sendは使うと警告が出る。何故かといえば、sendはよくありそうなメソッド名だからだ。だからObject#__send__を推奨するようにメッセージが出てくる。うっかりユーザがsendメソッドを追加してしまったらどこかでハマりになる恐れがあるような気もする。

load_environmentへ戻る。

(self.class.contants - constants).each do |const|
  Object.const_set(const, self.class.const_get(const))
end

Array#- は集合の差を返すメソッドである。2行前でconstをself.class.constantsとしたあとに、"config/environment.rb"を読み込んでいることから、"config/environment.rb"はInitializer::定数名を定義することを期待されているようだ。変更されたものだけ定数をセットする。こんな回りくどいやり方をしているのは定数に再代入しようとしたら例外が起こるからだろう。

evalを使っても出来るが、Module.const_getやModule.const_setを使った方が作法としては良いだろう。

initialize_logger

RAILS_DEFAULT_LOGGERが定義されていたら、何もせずに抜けるようだ。つまり独自のloggerを使いたければコンストラクタRAILS_DEFAULT_LOGGERを定義しておけば良いようです。でも、その処理はどこに書けば良いのだろう・・・。

initialize_dependency_mechanism

Configuration#cache_classesの値によって毎回読み込むか、一度のみ読み込むかを操作しているらしい。

development環境だと:loadモードでproduction環境だと:requireモードになるようだ。

これによりブラウザをリロードするだけでコードの変更を反映できるという開発の快適性を実現している。

Dependenciesクラスはaction_controller/dependencies.rbで定義されている。

詳しく読むのはまた次の機会にする。

initialize_whiny_nils

珍しくメソッド名から意味がよく分からない。activesupportのwhiny_nil.rbをrequireしているのでそちらへ飛ぶ。

このファイルをのぞいてみると、NilClassを拡張している。コメントを読むと、どうやらnilに伴うエラーの際にデバッグしやすくするための拡張のようだ。

  WHINERS = [ ::ActiveRecord::Base, ::Array ]
  
  @@method_class_map = Hash.new
  
  WHINERS.each do |klass| 
    methods = klass.public_instance_methods - public_instance_methods
    methods.each do |method|
      @@method_class_map[method.to_sym] = klass 
    end
  end

まず、あるクラスからObjectクラスに定義されているパブリックインスタンスメソッドの配列の差集合を取ることで独自に定義されているメソッド名を割り出している。

その後、メソッド名をキー、クラス名を値としたハッシュとしてキャッシュしておくことでメソッドを呼び出そうとしたがnilクラスのためメソッドが定義されてないと例外を発生させる以上のことをすることが出来る。

method_missingでraise_nil_warning_forを呼び出すことでユーザがどのクラスを使おうとしていたのかということまで教えてくれる。

ActiveRecord::BaseとArrayに対してこの機能を有効にしているのだけど、これが拡張できるとうれしいケースが他にもあるかもしれない。(ちょっと今は思いつかないけど)

initialize_temporary_directories

セッションとキャッシュを保存するディレクトリを初期化する。

initialize_framework_settings

それぞれのフレームワークの設定を初期化する。

    def initialize_framework_settings
      configuration.frameworks.each do |framework|
        base_class = framework.to_s.camelize.constantize.const_get("Base")

        configuration.send(framework).each do |setting, value|
          base_class.send("#{setting}=", value)
        end
      end
    end

frameworkはシンボルなので定数形式に変換し、〜::Baseとする。(:active_record → ActiveRecord::Base)

Object#__send__を利用してそれぞれのアクセサにアクセスする。それぞれのアクセサの値には前に見たようにOrderedOptionsクラスのインスタンスがセットされているので、eachを使うことが出来る。

eachの内部でまたObject#__send__を使っているが、OrderedOptionsクラスには引数のようなメソッドはなく、method_missingで処理を振り分けていることを思い出して欲しい。

after_initialize, after_initialize_block

Configurationクラスのafter_initializeメソッドで登録したブロックをコールバックする。

Configurationクラスのafter_initializeメソッドをみてみる。

    def after_initialize(&after_initialize_block)
      @after_initialize_block = after_initialize_block
    end
   
    def after_initialize_block
      @after_initialize_block
    end

Rubyでコールバック関数を定義するテクニックっぽい。

定義部分で

def method_name(&block)

と受けることでブロックはProcオブジェクトへ変換される。(yieldで受けた場合は変換されない。そのためyieldを使う方が若干速いという説がある)

ブロックは単なるProcオブジェクトになったのでインスタンス変数に代入できる。その結果、変数名.callするだけでコールバックすることが可能。(コールバックの意味合いがちょっと違うような気もする。)

これでとりあえずinitializer.rbはこれで終了。色々なテクニックが駆使されていてかなり面白かった。さすがにこれだけの量の文章を書くのは大変だったけど。

次はどこを読むかまだ決めていない。何かリクエストかお勧めがあればコメント・TBで教えてくださいな。