はじめに
難易度が比較的低く、初めてにおすすめのsettingslogicを読んでみることにしました。
settingslogicとは
簡単にいうと異なる値を環境ごとに持たせることができるようになるgemです。
https://github.com/settingslogic/settingslogic
動きの確認
まずは、applicationに以下のクラスを定義します。
Settinglogicを継承したSettingsクラスです。
ここでは、sourceとnamespaceの定義をしています。
class Settings < Settingslogic
source "#{Rails.root}/config/application.yml"
namespace Rails.env
end
Settingsクラスでsourceを指定していることにより、以下のファイルから値を読み込んでくれます。
# config/application.yml
defaults: &defaults
cool:
saweet: nested settings
neat_setting: 24
awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %>
development:
<<: *defaults
neat_setting: 800
test:
<<: *defaults
production:
<<: *defaults
[1] pry(main)> Settings.cool
=> {"saweet"=>"nested settings"}
[2] pry(main)> Settings.awesome_setting
=> "Did you know 5 + 5 = 10?"
ではいったいなぜこのようなことができるのでしょうか?
内部で何が起きているのか
Settings.cool
上記ではSettingクラスのcookメソッドを呼び出しています。
Settingsクラス、継承元のSettingslogicクラスにはcool
メソッド は定義されていないので、
missing_methodが呼び出されます。
渡された引数がinstance.send
に渡されています。
(send: nameをargsを引数にとり呼び出す = instance.cool(args))
def instance
return @instance if @instance
@instance = new
create_accessors!
@instance
end
def method_missing(name, *args, &block)
instance.send(name, *args, &block)
end
# It would be great to DRY this up somehow, someday, but it's difficult because
# of the singleton pattern. Basically this proxies Setting.foo to Setting.instance.foo
def create_accessors!
instance.each do |key, val|
create_accessor_for(key)
end
end
@instanceは最初の段階ではnilのため、
@instance = newでinitializeが呼び出されます。
以下では、Setting.rbに定義したsourceから、中身の情報を取り出しYAMLをハッシュ化しています。
def initialize(hash_or_file = self.class.source, section = nil)
#puts "new! #{hash_or_file}"
case hash_or_file
when nil
raise Errno::ENOENT, "No file specified as Settingslogic source"
when Hash
self.replace hash_or_file
else
file_contents = open(hash_or_file).read
hash = file_contents.empty? ? {} : YAML.load(ERB.new(file_contents).result).to_hash
if self.class.namespace
hash = hash[self.class.namespace] or return missing_key("Missing setting '#{self.class.namespace}' in #{hash_or_file}")
end
self.replace hash
end
@section = section || self.class.source # so end of error says "in application.yml"
create_accessors!
end
キーが存在すれば、fetch(key)
で値を取り出し、create_accessor_for
メソッド に渡しています。
def create_accessor_for(key, val = nil)
return unless key.to_s =~ /^\w+$/ # could have "some-setting:" which blows up eval
instance_variable_set("@#{key}", val)
self.class.class_eval <<-EndEval
def #{key}
return @#{key} if @#{key}
return missing_key("Missing setting '#{key}' in #{@section}") unless has_key? '#{key}'
value = fetch('#{key}')
@#{key} = if value.is_a?(Hash)
self.class.new(value, "'#{key}' section in #{@section}")
elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
value.map{|v| self.class.new(v)}
else
value
end
end
EndEval
end
これがぱっと見意味わかりませんでした。。。
まず、instance_variable_set
メソッドで@key
(今回は@cool)に値をセットします。
その後は、キーがメソッド名になったメソッドを定義しています。
大まかな流れ
-
method_missing
からinitialize`メソッド呼ばれる - @instanceがYAMLファイルをハッシュ化した物になる
-
create_accessors!
呼ばれ、keyがcreate_accessor_for
に渡される -
create_accessor_for
で動的メソッドを作成する - 動的メソッドは、ハッシュから値を取り出し、インスタンス変数に渡す
-
create_accessors
呼ばれる -
send
で動的にメソッドが定義されて呼び出される - 値が返る
知らなかった知識
- self.class.sourceの形がわからなかった
- console上で実行してもエラーとなるため、何が実行されているかわからなかった。
self.class
でクラスメソッドを呼び出しているので、今回の場合は実行されているのは、Settings.source
だった。
- console上で実行してもエラーとなるため、何が実行されているかわからなかった。
- evalとは
- メタプログラミングの一種で文字列をrubyのコードとして扱うことができるようになる。
- is_a(Hash)
- レシーバが引数のクラス、もしくはそのサブクラスから作成されたオブジェクトであればtrue
- fetch
- 引数にハッシュのキーを指定することにより、そのキーとセットになっているバリューを取り出す。
存在しない場合は例外を発生させる。
- 引数にハッシュのキーを指定することにより、そのキーとセットになっているバリューを取り出す。
- replace
- レシーバの値を引数の値に置き換えるメソッド
- instance_variable_set
p obj.instance_variable_set("@foo", 1) #=> 1
よく出てきた英単語
- accessors
- アクセサとは、オブジェクト指向プログラミングで、オブジェクト内部のメンバ変数(属性、プロパティ)に外部からアクセスするために用意されたメソッド。 メンバ変数をオブジェクト内部に隠蔽し、外部から直接参照させないようにするために用意される。
- create_accessorsはアクセサを作るメソッドということですね
まとめ
メタプロ難しすぎたんで、勉強しなおしてもう一回出直してこようと思います。
参考