Posted at
RubyDay 13

この冬はじめよう! 5分で理解するGemのコードリーディング(1) settingslogic

More than 3 years have passed since last update.


概要

Gemのコードリーディングで、広く使われている高品質なコードからテクニックや定石を学んでいきます。


はじめに

いきなりRailsとかRspecを読みにいくのもツラそうなので、ここでは初心者向けとウワサのsettingslogicを読んでいくことにします。Gemを読む際の難易度の目安としては、 初めてのGemの読み方を参考にさせて頂きました. :bow:

私自身は、いくつかのOSSで、放置されているissue(テストやドキュメンテーション)をお片付けしたことはありますが、普段、そんなにOSSにガッツリcommitしている訳でもありません。そんな私にとって、


  • メソッドの命名規則

  • メモ化、method chainの実現方法

  • コメントの書き方

など、Rubyのお作法を学ぶ題材として、またOSSのコードリーディングのきっかけとして、非常に良い題材だったと思います。


version

2.0.9を使用します。


settingslogicの使い方

ソースコードを読む前に、Settingslogicの使い方を確認しておきましょう。


設定ファイルの作成

まずは、管理する設定ファイルをYAML形式で記述します。


settings.yml

language:

haskell:
paradigm: functional
smalltalk:
paradigm: object oriented


設定内容にアクセスするクラスの定義

次に、設定ファイルの内容にアクセスするためのクラスを定義します。


settings.rb

class Settings < Settingslogic

source "#{File.dirname(__FILE__)}/settings.yml"
end


設定内容にアクセス

設定内容にアクセスするには、以下の様に記述します。

>Settings.language.haskell.paradigm

=> 'functional'

ただのHashよりも、扱いやすいインタフェースで設定内容にアクセスすることができます。

どうして、こんな事が可能になるのでしょうか?

上記のコードの裏側を理解することが今回のゴールです。


構成

ここは、軽く眺めるくらいで良いと思います。


$ tree -L 2 .
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.rdoc
├── Rakefile
├── lib
│   └── settingslogic.rb
├── settingslogic.gemspec
├── spec
│   ├── settings.rb
│   ├── settings.yml
│   ├── settings2.rb
│   ├── settings3.rb
│   ├── settings4.rb
│   ├── settings_empty.rb
│   ├── settings_empty.yml
│   ├── settingslogic_spec.rb
│   └── spec_helper.rb
└── vendor
└── bundle


コードリーディング


設定ファイルのパスを設定

まずは設定ファイル(YAML)のパス設定を行っている部分のソースコードをみてみます。


settings.rb

class Settings < Settingslogic

source "#{File.dirname(__FILE__)}/settings.yml"
end

class SettingsInst < Settingslogic
end



settingslogic.rb

def source(value = nil)

@source ||= value
end

sourceメソッドの中で、@sourceにYAMLファイルのパスが格納されます。

@sourceはメモ化されているようです。

この辺は読みやすいですね。


単なるHashよりも扱いやすいインタフェースで設定内容にアクセスする

では、いよいよ本題の部分の処理をみていきましょう。


Settings.languageメソッドが指定され、method_missingに処理が移る。

Settings.language.haskell.paradigm

>> 'functional'

上記のコードが実行されたとき、実際にはSettings.languageメソッドは存在しないので、method_missingが呼び出されます。

method_missingの引数であるnameには:languageが渡されます。

def method_missing(name, *args, &block)

instance.send(name, *args, &block)
end

次はinstanceメソッドの中身を見てみましょう。


YAMLファイルに記述された内容を読み込んでインスタンス作成

instanceメソッドでは、Settingsクラスのインスタンスであるオブジェクトを作成して返却します。

def instance

return @instance if @instance
@instance = new
create_accessors!
@instance
end

initializeメソッドの中身も少しのぞいてみます。

  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 # (1)
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 # (2)
end
@section = section || self.class.source # so end of error says "in application.yml"
create_accessors!
end

注目するのは (1)(2) の部分です。

(1)では、YAMLファイルに記述した内容をhashに変換しています。

そして (2) ではselfの内容をhashで置き換えます。

このとき、selfの中身を確認すると、こうなっています。

{

"language"=>{
"haskell"=>{"paradigm"=>"functional"},
"smalltalk"=>{"paradigm"=>"object oriented"}
}
}

今度は、initializerの中で呼ばれているcreate_accessors!を詳しくみていきます。


動的にアクセサを作成

このcreate_accessors!メソッドで、動的にアクセサを作成しています。

def create_accessors!

self.each do |key,val|
create_accessor_for(key)
end
end

selfの実体は、こんなHashでした。

{

"language"=>{
"haskell"=>{"paradigm"=>"functional"},
"smalltalk"=>{"paradigm"=>"object oriented"}
}
}

ですから、create_accessors!メソッドの引数のkeyには"language"が渡されます。

処理内容を理解する上で、このメソッドが最も重要な部分です。

  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}') # (3)
@#{key} = if value.is_a?(Hash)
self.class.new(value, "'#{key}' section in #{@section}") # (4)
elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
value.map{|v| self.class.new(v)}
else
value # (5)
end
end
EndEval
end

着目すべきはclass_evalの部分です。class_evalを用いることで、クラスメソッドの定義を動的に行っています。引数keyに'language'を当てはめて考えると、こうなります。

    self.class.class_eval <<-EndEval

def language
return @language if @language
return missing_key("Missing setting 'language' in #{@section}") unless has_key? 'language'
value = fetch('language')
@lanugage = if value.is_a?(Hash)
self.class.new(value, "'language' 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

Settings.languageにアクセスすると、 (3) の実行結果として、valueの中には、以下のような形でlanguage配下の情報が格納されます。

{"haskell"=>{"paradigm"=>"functional"}, "smalltalk"=>{"paradigm"=>"object oriented"}}

これはHashですから、 (4) が実行され、再びinitializeメソッドが実行され、その結果作成したインスタンスを返却します。ここで重要なことは、initializeメソッドの中では、再びHashのkey(ここでは"haskell"と"smalltalk")を元にアクセサを定義します。従って、Settings.languageで返却されるインスタンスには haskell, smalltalkというメソッドが定義されています。

確認してみましょう。

>Settings.language.methods

=>[
(略)
:haskell,
:smalltalk,
:method_missing,

ちゃんと定義されていますね。そして、

Settings.language.haskell が返すインスタンスは、{"paradigm"=>"functional"} を元に生成しますので、またまたparadigmメソッドが動的に定義されており、Settings.language.haskell.paradigm を呼ぶと、今度は (5) のコードが実行され、

>Settings.language.haskell.paradigm

=> 'functional'

という結果になります。


総括

Hashとclass_evalを組み合わせることで、YAMLで記述した設定内容に対して、より扱いやすいインタフェースでアクセスする方法を学びとることができました。

間違いなどございましたら、ご指摘ください :bow:

軽い気持ちで始めてみたGemのコードリーディングですが、これからも時間を見つけて投稿していきたいと思います。


参照