LoginSignup
8
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-10-20

インスタンス変数を作る&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

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

8
8
3

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
8
8