attr_readerとattr_accessorを拡張してto_hメソッドを簡潔に実装する
はじめに
先日Ruby on Railsであるサービスの開発を行っていた際に、自作のクラスにto_h
というインスタンスメソッドを実装する必要が出てきました。
このメソッドの要件は以下の通りです。
attr_reader
またはattr_accessor
で設定したgetterメソッドの名前をkey、戻り値をvalueとするハッシュを返す#to_h
メソッドの中には、属性名を直接書かないようにする(=勝手に上手いことやってくれるような書き方にする)
最初はObject#instance_variables
とObject#respond_to?
の組み合わせで以下のように実装しました。
class Hoge attr_accessor :attr1, :attr2 def to_h keys = instance_variables.flat_map do |val_name| getter_name = val_name[1..-1] respond_to?(getter_name) ? getter_name : [] end keys.map { |key| [key.to_sym, public_send(key)] }.to_h end end
これだと、getterメソッドは存在するが、同名のインスタンス変数が初期化されていない場合に、戻り値のハッシュから除外されてしまうという問題があります。
例えば以下のような感じです。
hoge = Hoge.new hoge.attr1 = 1 hoge.to_h # => {:attr1=>1}
上記の例の場合、@attr2
は初期化されていないため、#to_h
の戻り値には含まれないことになります。
そこで、これ以外の方法はないかと色々と調べたところ、それなりに納得のいく実装ができたので、備忘録も兼ねて記事にしました。
方針
基本的には、Ruby: attr_accessor を拡張する – CLARA ONLINE techblogと同じです。
上記リンク先での方針をまとめると、以下のようになります。
attr_accessor
をモジュールを使ってオーバライドし、渡された引数をクラスインスタンス変数として保持しておく#to_h
メソッド内で上記のクラスインスタンス変数を同名のクラスメソッド経由で取得し、戻り値のハッシュを組み立てる
方針はこれで問題ないのですが、リンク先の実装には以下のような不具合があります。
attr_accessor
がエラーで落ちた際の(不正な)引数を保持してしまうattr_accessor
に渡された同名の文字列とシンボル(例えば"attr1"
と:attr1
)を区別して保持してしまう- モジュールをincludeしたクラスAを継承した、
attr_accessor
の定義されていないクラスBをさらに継承したクラスCを作った場合、クラスAの属性がクラスCの#to_h
の戻り値に含まれない
3つ目はちょっと複雑ですが、これはsuperclass
に対してinstance_variable_get
を使った呼び出しを行っていることに起因しています。
詳しく知りたい方は、リンク先の実装を見ていただければと思います。
実装
こんな感じでいくつかの不具合があることがわかったので、これらを解消する実装を考えてみました。
具体的には以下の通りです。
このAttrAccessorExtension
モジュールを任意のクラスでinclude
することで、当初の要件が実現できます。
おわりに
良い頭の体操になりました。