Settingslogicをコードリーディングしてみる

最近メタプログラミングRubyを読み始めて、実務的なRubyのテクニックを知りたくなったため、Gemのコードリーディングをしてみました。

初心者なので、以下記事で勧められていた中でも一番簡単なSettingslogicを選択。
参考：[RubyGemコードリーディングのすすめ](https://blog.freedom-man.com/try-rubygem-codereading)

Settingslogicのリポジトリは[こちら](https://github.com/settingslogic/settingslogic)

# 準備
動作確認用にpryを利用できるようにする

```Gemfile
source 'https://rubygems.org'
gemspec

gem 'pry-doc' #追加
gem 'pry-byebug' #追加
gem 'byebug' #追加
```

```
bundle install
```

```settingslogic.rb
require 'pry' # 追加
```

ローカルのruby環境によってはpryが読み込めない場合があるが、たくさん記事があるので割愛。


# 事前知識 

#### ||=
「左辺が未定義または偽なら右辺の値を代入する」の意。

#### str =~ regexp
文字列strに対する正規表現regexpでのパターンマッチング。

```
str = "hello <ruby> world"
if str =~ /<(\w+)>/
  puts $1
end
```

参考：[=~ (String) - Rubyリファレンス](https://ref.xaio.jp/ruby/classes/string/match_operator)


#### \w
単語構成文字
`[a-zA-Z0-9_]`
を表すメタ文字。

#### 特異クラス

```ruby
class << self
```

上記は特異クラスといい、クラスメソッドの定義を簡易化するためのもの。
特異クラスを利用すればクラスメソッドの定義にselfをつける必要がなくなる。

```ruby
# 特異クラスを利用した場合
class << self
  def hoge; end
  def foo; end
end

# 特異クラスを利用しない場合
def self.hoge; end
def self.foo; end
```

```ruby
def method_missing(name, *args, &block)
  instance.send(name, *args, &block)
end
```

上記は、継承チェーン内にメソッドが見つからなかった場合の処理（method_missing）をオーバーライドしている。


#### method_missing(name, *args)
継承チェーン内に指定されたメソッドが見つからなかった場合に呼ばれるメソッド。

#### send(name, *args)
引数（name）で指定されたレシーバが持つメソッドを実行する。
これを利用することで、動的にメソッドを呼ぶことができる。

#### 演算子式の再定義
演算子式は再定義が可能。

```ruby
# 二項演算子
def +(other)                # obj + other
def -(other)                # obj - other

# 単項プラス/マイナス
def +@                      # +obj
def -@                      # -obj

# 要素代入
def foo=(value)             # obj.foo = value

# [] と []=
def [](key)                 # obj[key]
def []=(key, value)         # obj[key] = value
def []=(key, key2, value)   # obj[key, key2] = value

# バッククォート記法
def `(arg)                  # `arg` または %x(arg)
```

参考：[クラス／メソッドの定義 (Ruby 2.6.0)]
(https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html)


#### シングルトンパターン
クラスが生成するオブジェクトを一つに制限する設計手法。
クラスのインスタンス変数はオブジェクトごとに異なるため、システムが大きくなるとバグ（意図しないインスタンス変数の変更）が起きやすくなる。
それに対し、シングルトンパターンを用いればオブジェクト管理がしやすくなる。



# コードリーディング

## 独自例外処理
```ruby:settingslogic.rb
class MissingSetting < StandardError; end

  ...

def missing_key(msg)
  return nil if self.class.suppress_errors

  raise MissingSetting, msg
end
```

いくつも例外がある中で`StandardError`クラスを利用している理由は、`rescue`の引数を省略した場合に`StandardError`クラスを指定したものとして処理されるため。
（`raise`の場合は`RuntimeError`クラス）
参考：[Rubyで独自例外を定義するときはStandardErrorを継承する - Hack Your Design!](https://blog.toshimaru.net/ruby-standard-error/)


## 動的なアクセサ定義
`Settingslogic#get`では`send`を使って動的なアクセサ定義を実現している。
こうすることで、いちいち

```ruby
def getA ...
def getB ...
def getC ...
```

といった冗長な各種インスタンス変数へのアクセサ定義を省略できるため、コード量を抑えることができる。
また、仕様変更により新たなインスタンス変数が追加された場合でも、アクセサ定義を追加する手間が省けるというメリットもある。


アクセサの必要性については以下参照。
[アクセサ（アクセスメソッド）とは - IT用語辞典 e-Words](http://e-words.jp/w/%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B5.html)


## initialize

```ruby:settingslogic.rb
def initialize(hash_or_file = self.class.source, section = nil)
  ...
end
```

`initialize`ではその名の通り初期化を行う。
第一引数のデフォルトは`self.class.source`となっているが、これはクラスメソッドをインスタンスメソッドから呼ぶための記法。

参考：[https://qiita.com/Linda_pp/items/b7135ae1f0def6058c6c:title]

```ruby:settingslogic.rb
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
```

第一引数がHashであれば`self`、つまりレシーバーである`Settingslogic`と置き換える。
そうでなければ指定ファイルの中身をチェックして、Hashにして返す。
（`Settingslogic`はHashを継承したスーパークラスなので、selfのreplaceメソッドはHashのreplaceメソッドである）

次に、ラストの`create_accessors!`で何が行われているかを見ていく。

## create_accessors!
```
def create_accessors!
  self.each do |key,val|
    create_accessor_for(key)
  end
end

def create_accessor_for(key, val=nil)
  ...
end
```

`initialize`の最後に`create_accessors!`を実行している。
`create_accessors!`はクラスメソッドとインスタンスメソッドの二つがあるが、上述した通り、クラスメソッドをインスタンスメソッド（initialize）から呼ぶ場合は`self.class.create_accessors!`という形で書かなければならないため、ここではインスタンスメソッドが実行されることになる。
（クラスメソッドの方はprivateなので、厳密にはまた別の問題がある）

`create_accessors`内の`self`は`initialize`で格納したHashで、eachで繰り返し`create_accessor_for`に渡している。
`create_accessor_for`では受け取ったkey, valueを`instance_variable_set`でレシーバーのインスタンス変数としてセットしている。

```ruby:settingslogic.rb
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
```

また、`class_eval`で動的にkeyの値を取得するクラスメソッドを生成するようにしている。

該当のクラスメソッドは、valueがHashであればオブジェクトを生成し、valueがArrayであれば中身をチェックし、中身が全てHashであれば、それぞれの値をもとにオブジェクトを生成し、そうでなければvalueを返すものとなっている。

ここで、`class_eval`の必要性を考える。

> class_evalメソッドは、ブロックをクラス定義やモジュール定義の中のコードであるように実行します。ブロックの戻り値がメソッドの戻り値になります。

参考：[class_eval, module_eval (Module) - Rubyリファレンス](http://ref.xaio.jp/ruby/classes/module/class_eval)

つまり、`class_eval`を利用することで、クラスメソッドを定義する処理をインスタンスメソッド内で実現している。

# まとめ
おそらく内容的にはメタプログラミングRubyの第5章くらいまで読んでいればなんとなく理解できるものと思います。

独学しているだけでは上記のようなコードを書くこと難しいと思うので、コードリーディングは定期的にしていきたい。

コードリーディングにおすすめのGemがあれば、教えていただけると幸いです:bow_tone1: