Rubyのお勉強がてら、Settingslogicのコードリーディングをしてみました。
Settingslogicとは
ファイルから設定を読み込んでコード内で利用できるようにする便利gemです。 使い方は$ gem install settingslogic
してからSettingslogicを継承してsourceとnamespaceを定義すればOK
require 'settingslogic'
class Settings < Settingslogic
source "./application.yml"
namespace "development"
end
Settings.hoge # => 800
sourceのapplication.ymlはこんな感じで記述すればOK
defaults: &defaults
foo:
bar: nested settings
hoge: 24
awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %>
development:
<<: *defaults
hoge: 800
test:
<<: *defaults
production:
<<: *defaults
見ての通り、YAMLは環境ごとのセクションに分かれて記述できて、さらにERB形式でRubyコードを埋め込めます。
あとは継承したクラス(今回はSettings)のメソッドとして設定値を取得できます。
コードリーディングの準備
gem installでグローバルにインストールするのではなく$ bundle init
### Gemfile編集
$ bundle install --path=vendor/bundle
で、ローカルのプロジェクトに対してgemをインストールし、そのgemに対してデバッグを貼ります。こうすることでグローバルのgemには影響なく安全にコード解析のためのコード改変をすることができます。
また、Gemfileは以下のようにpry+pry-byebugを入れます。これにより、binding.pryのデバッガを仕込んで、backtraceによるスタックトレースの表示ができます。
gem "settingslogic"
gem "pry"
gem "pry-byebug"
ローカルのgemのパスは{APPLICATION_PATH}/vendor/bundle/ruby/{RUBY_VERSION}/gemsとなります。今回の場合はsettingslogicを弄るので、{APPLICATION_PATH}/vendor/bundle/ruby/{RUBY_VERSION}/gems/settingslogic-2.0.9/lib/settingslogic.rbにデバッグを仕込んでスタックトレースをしながら、挙動確認をしていく形になります。
今回の場合はSettingsLogicクラスのクラスメソッドのmethod_missingやinitializeに対してbinding.pryを仕込んでコードを追っていきました。
コードリーディング
Settingslogicの大まかな流れは以下のようになります。(継承したクラス名をSettingsとして記述します。)- Settings.xxxでSettingslogicクラスを継承したSettingsクラスに対するxxxメソッドを呼び出す。
- selfがSettingsクラスであるself.method_missing(class << selfコンテキスト内のmethod_missing)が定義されており、xxxメソッドは最初存在しないため、当メソッドが呼び出される。
- self.method_missingメソッド内でintance.sendを呼び出すが、まずはinstanceメソッドが呼び出される。
- @instanceは最初nilなのでSettingsオブジェクトのインスタンスが作成される。
- Settingsオブジェクトのインスタンス作成でコンストラクタのinitializeが呼び出される。argument無しなので指定されている初期値で動作する。self.class.sourceはSettingsクラス定義時に設定した値(上記例の場合は”./application.yml”)
- ファイル指定の場合は、intialize内でYAML.load(ERB.new(file_contents).result).to_hashによるYAMLのハッシュ化が行われる。namespaceが定義されていれば、namespaceのvalue値をHashのKey/Valueとして自インスタンスの値を書き換える(SettingslogicはHashを継承しているのでSettingsクラス自身がHashである)
- create_accessors!でHashのキー値をメソッド化する。この処理はHashのキーに対してのみ行われるのでネストした下位の値はメソッド化しない。
- create_accessor_forで各キーに対してメソッドが定義される。self.class.class_evalはSettingsのコンテキストで評価されることになるので、Settingsのインスタンスのメソッドとして機能する。インスタンス変数にはnilをセットする。
- 定義した動的メソッドはインスタンス変数がnilでなければインスタンス変数を返す。nilの場合はHashから値を取り出してその値を渡す+インスタンス変数にセットする。初回呼び出しはnilだが、二回目以降はインスタンス変数がセットされるのでHashからのfetchは発生しない。値がHashである(=ネストしている)場合は、返す値をSettingsインスタンスとしている。(Hashの場合のinitializeはHashのKey/Vaue値をそのまま自身に反映するだけ)。
- Settingsクラスのコンテキストに戻って、create_accessors!が実行され、Settingsに対して動的にメソッドが定義される。このメソッドはinstance.send(:key)だけの呼び出しとなっており、インスタンスに対してメソッドを呼び出している。これによってSettings.xxxでSettingsインスタンス(Settingsクラスのクラスインスタンス変数)のxxxメソッドが呼び出されるようになる。
- 最後にinstanceのsendが呼び出され、最終的に9で定義した動的メソッドが呼び出される。
動的メソッドの定義はアクセス時に階層ごとに一回ずつ行われ、二回目以降は定義された動的メソッドを見るようになります。
コードリーディングから得た知見
- method_missingはコストが高いので、初回アクセス時に同一階層のキーに対する全ての動的メソッドを作るようにして、次回以降は動的メソッドを呼び出すようにすれば、初回のコストだけを気にすれば良いので、全体的なパフォーマンス上は良さそう。
- YAML.load(ERB.new(file_contents).result).to_hashの記述は他の設定ファイル定義でも利用できそう。
- ネストする構造を定義したい場合はKey => Valueを一つのオブジェクトとして見立てて、インスタンスをネストする形で定義するのがスマートな感じ。
- クラスインスタンス変数はインスタンスからはアクセスできないので、クラスメソッドから限定的にアクセスされる手段を提供するという条件においては安全なコーディングが出来る設計にできそう。