Edited at

インスタンス変数とアクセッサメソッドを同時に作る

More than 3 years have passed since last update.

インスタンス変数を作る&attr_readerすることは多いと思います。

2014/11/26 コメントにてリファクタリングして頂きました!言われてみればなぜわざわざeachでattr_readerしていたのか・・・。そしてsendは完全に頭から抜け落ちていました。

2014/11/28 おまけのarr_setterをコメントでのリファクタリング&可変長引数に変更。可変長にすると使う時にArrayを渡すわけではなくなるけど気にしない。


attr_setter.rb

module AttrSetter

def attr_setter(*syms)
syms.each do |sym|
val = yield sym
instance_variable_set("@#{sym}", val)
end
class.send(:attr_reader, *syms)
end
end

class XmlItem
def initialize(xml_str)
include AttrSetter
doc = REXML::Document.new(xml_str)

arr_attr_setter(:title, :date, :descritpion, ...) do |sym|
get_text(doc, sym.to_s)
end
end
end



経緯

( ◠‿◠ )XMLパースをします

(´・‿・`)attr_readerと要素の抽出がDRYに反してる

( ◠‿◠ )メタプロをします

✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌たのしい!


ビフォー

以下はXMLをパースして、その結果を外に公開するクラスを作っていた時に最初に書いたものです。


(‘ᾥ’#).rb

require 'rexml/document'

class XmlItem
attr_reader :title, :date, :description ... #15個ぐらい要素が続く
def initialize(xml_str)
doc = REXML::Document.new(xml_str)

@title = get_text(doc, "title")
@date = get_text(doc, "date")
@description = get_text(doc, "description")
.
.
.#15個ぐらい@hoge = get_text(doc, "hoge")が続く
end

def get_text(doc, tag)
doc.text(tag)
end
end


やめてくださいしんでしまいます(^q^)

XMLをパースした結果から必要なものを抜き出してそれを外部へ公開するためのアクセサメソッドを書いたコードです。

似たようなコードがズラズラと並んでいます。

精神衛生上非常に悪いです。一刻も早く消し去り、心の平穏を取り戻しましょう。


アフター

リファクタリングした結果


(^ω^).rb

module AttrSetter

def attr_setter(sym, val = nil, &block)
val = yield sym if block_given?
instance_variable_set('@' + sym.to_s, val)
self.class.class_eval do |_|
attr_reader sym
end
end
end

class XmlItem
include AttrSetter
def initialize(xml_str)
doc = REXML::Document.new(xml_str)

[:title, :date, :description ...].each do |sym|
attr_setter(sym, get_text(doc, sym.to_s))
end
end

def get_text(doc, tag)
doc.text(tag)
end
end


なんということでしょう。あんなにも散らかっていたコードが匠の手によってこんなにも綺麗になりました。

attr_setterというメソッドによって、要素の抽出とアクセッサの追加が同時に行えるために:title, :dateなどを一つにまとめることができました。

先ほど使い方ではブロックを使っていませんが、ブロックを使うことでスコープを制限した中で束縛するオブジェクトを作れます。


block.rb


#get_text(doc, "piyopiyo")の結果にpiyoでアクセスできる
attr_setter(:piyo) do |sym|
piyopiyo = sym.to_s * 2
get_text(doc, piyopiyo)
end



解説


(^ω^).rb

module AttrSetter

def attr_setter(sym, val = nil, &block)
val = yield sym if block_given?
instance_variable_set('@' + sym.to_s, val)
class.class_eval do |_|
attr_reader sym
end
end
end

attr_setterはインスタンス変数の名前となるシンボル(文字列でもいいけど慣習上シンボルということで)とそのインスタンス変数に束縛する値を受け取ります。

valのデフォルト引数にnilを与えているのは

attr_setter(:sym) do |sym|

val
end

と書けるようにするため。

ブロックがあれば、ブロックの結果を優先してvalに代入します。

その後、instance_variable_setでインスタンス変数を作り、

self.class.class_evalからattr_readerでアクセッサを定義しています。


結論

メタプログラミングは楽しいです


おまけ

何度もself.class.class_evalするのが気になるなら


omake.rb

module AttrSetter

def arr_setter(*syms)
syms.each do |sym|
val = yield sym
instance_variable_set('@' + sym.to_s, val)
end
self.class.class_eval do |_|
syms.each do |sym|
attr_reader sym
end
end
end
end

class XmlItem
def initialize(xml_str)
include AttrSetter
doc = REXML::Document.new(xml_str)

arr_attr_setter([:title, :date, :descritpion, ...]) do |sym|
get_text(doc, sym.to_s)
end
end
end


というのもアリかもしれない。