最近メタプログラミングRubyを読み始めて、実務的なRubyのテクニックを知りたくなったため、Gemのコードリーディングをしてみました。
初心者なので、以下記事で勧められていた中でも一番簡単なSettingslogicを選択。
参考:RubyGemコードリーディングのすすめ
Settingslogicのリポジトリはこちら
準備
動作確認用にpryを利用できるようにする
source 'https://rubygems.org'
gemspec
gem 'pry-doc' #追加
gem 'pry-byebug' #追加
gem 'byebug' #追加
bundle install
require 'pry' # 追加
ローカルのruby環境によってはpryが読み込めない場合があるが、たくさん記事があるので割愛。
事前知識
||=
「左辺が未定義または偽なら右辺の値を代入する」の意。
str =~ regexp
文字列strに対する正規表現regexpでのパターンマッチング。
str = "hello <ruby> world"
if str =~ /<(\w+)>/
puts $1
end
\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)
シングルトンパターン
クラスが生成するオブジェクトを一つに制限する設計手法。
クラスのインスタンス変数はオブジェクトごとに異なるため、システムが大きくなるとバグ(意図しないインスタンス変数の変更)が起きやすくなる。
それに対し、シングルトンパターンを用いればオブジェクト管理がしやすくなる。
コードリーディング
独自例外処理
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
def initialize(hash_or_file = self.class.source, section = nil)
...
end
initialize
ではその名の通り初期化を行う。
第一引数のデフォルトはself.class.source
となっているが、これはクラスメソッドをインスタンスメソッドから呼ぶための記法。
参考:[https://qiita.com/Linda_pp/items/b7135ae1f0def6058c6c:title]
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
でレシーバーのインスタンス変数としてセットしている。
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があれば、教えていただけると幸いです