89
82

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RubyAdvent Calendar 2015

Day 13

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

Posted at

概要

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

参照

89
82
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
89
82

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?