LoginSignup
2
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-31

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

初心者なので、以下記事で勧められていた中でも一番簡単なSettingslogicを選択。
参考:RubyGemコードリーディングのすすめ

Settingslogicのリポジトリはこちら

準備

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

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リファレンス

\w

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

特異クラス

class << self

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

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

# 特異クラスを利用しない場合
def self.hoge; end
def self.foo; end
def method_missing(name, *args, &block)
  instance.send(name, *args, &block)
end

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

method_missing(name, *args)

継承チェーン内に指定されたメソッドが見つからなかった場合に呼ばれるメソッド。

send(name, *args)

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

演算子式の再定義

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

# 二項演算子
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)

シングルトンパターン

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

コードリーディング

独自例外処理

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!

動的なアクセサ定義

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

def getA ...
def getB ...
def getC ...

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

アクセサの必要性については以下参照。
アクセサ(アクセスメソッド)とは - IT用語辞典 e-Words

initialize

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]

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内のselfinitializeで格納したHashで、eachで繰り返しcreate_accessor_forに渡している。
create_accessor_forでは受け取ったkey, valueをinstance_variable_setでレシーバーのインスタンス変数としてセットしている。

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リファレンス

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

まとめ

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

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

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

2
0
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
2
0