Rails::Initializerを読む。
目次はこちらです。
まずは、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_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クラスのインスタンスを渡している。
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で教えてくださいな。