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

  • 59
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

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のコードリーディングですが、これからも時間を見つけて投稿していきたいと思います。

参照

この投稿は Ruby Advent Calendar 201513日目の記事です。