かずおの開発ブログ(主にRuby)

日々の開発のことを色々書きます。

オブジェクト生成時にインスタンス変数の初期値を設定する

オブジェクト生成時にインスタンス変数の初期値を設定する Railsのmodelではmodel生成時に

user = User.new(name: "tarou", address: "東京都港区", phone: "08099999999"...)

このような形で初期値を設定することが出来ます。

でこれに慣れてるとRubyでこのようなクラスがあったとして、同じようにこんなふうにインスタンス変数設定したいなーと思って書いてもRubyのオブジェクトにそんな機能は備わってないので、Object#initializeを呼んでエラーになってしまいます。

class Bar
  attr_accessor :a, :b, :c
end
Bar.new(a: "A", b: "B", c: "C")
ArgumentError: wrong number of arguments (1 for 0)
from (pry):5:in `initialize'

よくあるやり方としてはinitializeで普通に引数を設定する方法があります。

class Foo
  attr_accessor :a, :b, :c
  def initialize(a, b, c)
    @a = a
    @b = b
    @c = c
  end
end
=> :initialize
Foo.new("A", "B", "C")
=> #<Foo:0x007fec0b61e690 @a="A", @b="B", @c="C">

ただしこのやりかただと変数の数がたくさん増えていった場合にいちいちinitializeの定義を確認してどの順番で引数を渡さないといけなかったかなーと確認しないといけなくなりとても不便です。

そこでもう少し便利な方法を考えます。

キーワード引数を使う

キーワード引数を使うことで引数を関数に与えられた順番ではなく、キーワードに紐付けて渡せるようになります。その結果上記のコードは以下のように書き直せます。これによってどの値にどの値をセットしているか がよくわかるようになるし、順番も気にする必要がなくなります。

class Bar
  attr_accessor :a, :b, :c
  def initialize(a:, b:, c:)
    @a = a
    @b = b
    @c = c
  end
end
Bar.new(a: "A", b: "B", c: "C") #keyword:値 なのでどの値にどの値をセットするかがわかりやすい
=> #<Bar:0x007fec06199c50 @a="A", @b="B", @c="C">
Bar.new(b: "B", a: "A", c: "C") #順番を入れ替えても問題ない
=> #<Bar:0x007fec0925c388 @a="A", @b="B", @c="C">

ただしこれでもたくさん変数があると

def initialize(a:, b:, c:, d:...)
  @a = a
  @b = b
  @c = c
  @d = d
...
end

となってやや冗長な感じがします。

sendを使う

そこで今度はsendを使って初期値の代入を行います。与えられた引数をまずhash形式に直したうえでsendを使って初期値をセットします。こうすることでいくら初期値がたくさんあってもこれ以上コードが長くなることはなくなりました。ただし、ぱっと見何をやってるかわかりづらいのでここまでやるかは微妙なところかと思います。

class Foo
  attr_accessor :a, :b, :c, :d, :e, :f
  def initialize(a:, b:, c:, d:, e:, f:)
    # 与えられた引数をhash形式で取得
    args_hash = self.method(__callee__).parameters.map do |arg_type,arg|
      [ arg, eval(arg.to_s) ]
    end.to_h
    # sendを使って初期値をセット
    args_hash.each do |prop_name, prop_value|
      self.send("#{prop_name}=",prop_value)
    end
  end
end
Foo.new(a: "A", b: "B", c: "C", d: "D", e: "E", f: "F")
=> #<Foo:0x007f8154748128 @a="A", @b="B", @c="C", @d="D", @e="E", @f="F">
Foo.new(a: "A", b: "B", c: "C")
ArgumentError: missing keywords: d, e, f #何が足りていないかもわかりやすい。ただし何も入れたくない場合は明示的にnilをセットする必要がある。

まだまだ初学者+エンジニア1人でやっているのでおかしいところがあれば是非ご指摘いただけると嬉しいです。 参考: http://qiita.com/hidechae/items/5416a996529d7663c182