Matthewの備忘録

忘れたときはここを見ろ。何か書いてある。

RubyまたはRoRで動的にアクセサーを生成・削除する その1

 シンプルに考えれば、ファクトリーメソッドパターンで条件にあったクラスのインスタンスを作ればよいだけなのだが、別の解決方法もあったので備忘録として記しておく。

静的アクセサー

 Ruby では次のようにクラスに書くとクラスインスタンスを生成したときにインスタンス変数とゲッター/セッターメソッドを自動的に生成してくれる。

attr_accessor :hoge

コンストラクト時に動的にアクセサーを設ける

 クラスに記述した静的なアクセサーしか使えないと思っていたがそんなことはなかった。10年以上前に考えていた方がいた。

d.hatena.ne.jp

インスタンス変数 @attr を格納先とし、ハッシュとして渡されたアクセサー名から格納する変数とそのアクセスメソッドを生成している。

 ruby 2.2.6 以上で実行できた。最新版で確認することは重要である。但し、時間的な生成コストやリスクについては後日調べることにする。

オブジェクトに動的にアクセサーを設ける

 前出のブログ記事で紹介されいたのはコンストラクターの引数にハッシュを与えて幾つでも好きなだけアクセサーを加えることだった。実は、前での方法はコンストラクターでなくても可能であり、適当なアクセサー生成メソッド名を決めておけばオブジェクトにアクセサーを加えることもできた。

#! /usr/bin/env ruby
#coding: UTF-8

class MyClass
#  def initialize(h = {})
#    @attr = h.dup
#    h.keys.each do |prop|
#      self.instance_eval %Q{
#        def #{prop.to_s}
#          @attr["#{prop}".intern] || ""
#        end
#        def #{prop.to_s}=(arg)
#          @attr["#{prop}".intern]= arg
#        end
#      }
#   end
#  end
  def make_accessor(h = {})
    @attr = h.dup
    h.keys.each do |prop|
      self.instance_eval %Q{
        def #{prop.to_s}
          @attr["#{prop}".intern] || ""
        end
        def #{prop.to_s}=(arg)
          @attr["#{prop}".intern]= arg
       end
      }
    end
  end
end

#obj = MyClass.new({:test => "dynamic access"})
obj = MyClass.new
#puts obj.test
obj.make_accessor({:one => "1", :two => "2", :three => "3"})
puts obj.one
puts obj.two
puts obj.three
obj.one = "11"
puts obj.one
obj.make_accessor({:one => "111"})
puts obj.one

# 以下は実行結果
# 1
# 2
# 3
# 11
# 111

 ハッシュを上書き可能にしてあるので、条件によって扱いたいアクセサーを変えたい場合に便利かもしれない*1。またコンストラクタに手を入れたくないときは、このように適当に名付けたアクセサー生成メソッドを作ったほうがよい。

既にアクセサーが登録してあれば、defined?などを使って例外を出すなどの処理を加えてもよいだろう。@attrの初期化位置/タイミングはそこでいいのか後日調べることにする。

*1:そういうときは異なるクラスのオブジェクトを生成したほうがよいかも?