動かざることバグの如し

近づきたいよ 君の理想に

Rubyでattr_accessorを動的に追加したい

Rubyなんもわからん

環境

経緯

例えば以下のようなUserクラスがあったとする。ここでは検証のために attr_reader ではなく attr_accessor を使っているのでご了承

class User

  attr_accessor :name, :age

  def initialize
    @name = "太郎"
    @age = 20
  end

  def name
    "#{@name}さん"
  end

end

この状態で、以下を出力実行

u = User.new
puts u.name # => 太郎さん
puts u.age # => 20

まぁ納得である。

ここでは attr_accessor :name, :age と予め書いたが、このnameとageが変則でインスタンス化されたクラスごとに動的に定義させたい場合はどうすればいいのか

Rubyの大抵は send() を使うと解決するってマックのJKが言ってた。

class User

  def initialize
    self.class.send(:attr_accessor, "name")
    self.instance_variable_set("@name", "太郎")
    self.class.send(:attr_accessor, "age")
    self.instance_variable_set("@age", 20)
  end

  def name
    "#{@name}さん"
  end
end

一見動きそうだが、動かない 実際に試してみると

u = User.new
puts u.name # 太郎 (????
puts u.age # 20

おかしい。インスタンス変数で定義したのが反映されれば本来「太郎さん」になるはず

おそらく、attr_accessorでnameのセッターが自動で定義されるので、 def name が上書きされてしまったんじゃないか。。。

変数をセットする側の self.instance_variable_set("@name", "太郎")@name = "太郎"self.send("name=", "太郎") に変更しても変わらずだった。。

うまく行った例

instance_variable_set("@#{k}", v)
unless respond_to? k
  define_singleton_method(k) { instance_variable_get("@#{k}") }
end

完成サンプルも載せとく

# 完成版
class User

  def initialize(args = [])
    args.each do |k ,v|
      instance_variable_set("@#{k}", v)
      unless respond_to? k
        define_singleton_method(k) { instance_variable_get("@#{k}") }
      end
    end
  end

  def self.fetchs
    # APIで取得したのを想定
    [
      {name: "太郎", age: 20 },
      {name: "次郎", age: 30 }
    ].map do |hash|
      User.new(hash)
    end
  end

  def name
    "#{@name}さん"
  end

end

User.fetchs.each do |a|
  puts a.name
  puts a.age
end

すると

# 太郎さん
# 20
# 次郎さん
# 30

と表示される。

追記

やはり attr_accessor がゲッターを上書きしてしまってる説は正しかった。以下でもうまく動作する。

class User

  def initialize
    self.class.send(:attr_reader, "name") unless respond_to? "name"
    self.class.send(:attr_writer, "name") unless respond_to? "name="
    self.class.send(:attr_reader, "age") unless respond_to? "age"
    self.class.send(:attr_writer, "age") unless respond_to? "age="
    self.instance_variable_set("@name", "太郎")
    self.instance_variable_set("@age", 20)
  end

  def name
    "#{@name}さん"
  end

end

u = User.new
puts u.name # => 太郎
puts u.age # => 20

書き直したテンプレも一応入れとく

class User

  def initialize(args = [])
    args.each do |k ,v|
      self.class.send(:attr_reader, k) unless respond_to? k
      instance_variable_set("@#{k}", v)
    end
  end

  def self.fetchs
    # APIで取得したのを想定
    [
      {name: "太郎", age: 20 },
      {name: "次郎" }
    ].map do |hash|
      User.new(hash)
    end
  end

  def age
    @age.to_i
  end

end

User.fetchs.each do |a|
  puts a.name
  puts a.age
end