35
class Foo
  attr_accessor :name, :age, :email, :gender, :height

  def initalize params
    @name = params[:name]
    @age = params[:age]
    @email = params[:email]
    .
    .
    .
  end

This seems like a silly way of doing it. What is a better/more idiomatic way of initalizing objects in Ruby?

Ruby 1.9.3

B Seven
  • 44,484
  • 66
  • 240
  • 385
  • 1
    I actually think the answer to this question is 'no' - there might be better ways, but if there was a better _and more idiomatic_ way then there would be an unequivocally up-voted answer below. – Ross Attrill Jan 07 '14 at 01:18

8 Answers8

43

You can just iterate over the keys and invoke the setters. I prefer this, because it will catch if you pass an invalid key.

class Foo
  attr_accessor :name, :age, :email, :gender, :height

  def initialize params = {}
    params.each { |key, value| send "#{key}=", value }
  end
end

foo = Foo.new name: 'Josh', age: 456
foo.name  # => "Josh"
foo.age   # => 456
foo.email # => nil
Joshua Cheek
  • 30,436
  • 16
  • 74
  • 83
  • 1
    I like this as well, but it seems like it's only useful in classes where all of the instance variables have setters. – Zach Kemp Oct 06 '12 at 19:29
17
def initialize(params)
  params.each do |key, value|
    instance_variable_set("@#{key}", value)
  end
end
Dorian
  • 22,759
  • 8
  • 120
  • 116
Zach Kemp
  • 11,736
  • 1
  • 32
  • 46
  • 8
    I dislike this in general, but if you're going to use it, at least invoke setters rather than setting instance variables. If you mess up the name of the variable using ivars, you won't know it, but using methods, you'll get a NoMethodError. – Joshua Cheek Jan 04 '14 at 08:07
  • 1
    @Joshua: That's perfectly valid. I agree that your method is generally preferable, however, I don't always provide setters for all instance variables (for example, when they shouldn't change after object initialization). – Zach Kemp Jan 05 '14 at 08:46
  • 1
    lol, didn't even realize I'd answered in this thread. Anyway, I nearly always access via setters/getters. If you don't want them to change, then declare them as private ([example](https://github.com/JoshCheek/seeing_is_believing/blob/3161fb906d38ebb4300333f20d84bd58fa3e7652/lib/seeing_is_believing/binary/commentable_lines.rb#L32-37)) But really, initializer code like this should only be used on data structures, so this issue shouldn't come up in practice. – Joshua Cheek Jan 05 '14 at 14:29
17

To capitalize on Joshua Cheek's answer with a bit of generalization

module Initializable
  def initialize(params = {})
    params.each do |key, value|
      setter = "#{key}="
      send(setter, value) if respond_to?(setter.to_sym, false)
    end
  end
end

class Foo
  include Initializable

  attr_accessor :name, :age, :email, :gender, :height
end

Foo.new name: 'Josh', age: 456
=> #<Foo:0x007fdeac02ecb0 @name="Josh", @age=456>

NB If the initialization mix-in has been used and we need custom initialization, we'd just call super:

class Foo
  include Initializable

  attr_accessor :name, :age, :email, :gender, :height, :handler

  def initialize(*)
    super

    self.handler = "#{self.name} #{self.age}"
  end
end

Foo.new name: 'Josh', age: 45
=> #<Foo:0x007fe94c0446f0 @name="Josh", @age=45, @handler="Josh 45"> 
Vadym Tyemirov
  • 8,288
  • 4
  • 42
  • 38
9
Foo = Struct.new(:name, :age, :email, :gender, :height)

This is enough for a fully functioning class. Demo:

p Foo.class # Class

employee = Foo.new("smith", 29, "smith@foo.com", "m", 1.75) #create an instance
p employee.class # Foo
p employee.methods.sort # huge list which includes name, name=, age, age= etc
steenslag
  • 79,051
  • 16
  • 138
  • 171
4

Why not just explicitly specify an actual list of arguments?

class Foo
  attr_accessor :name, :age, :email, :gender, :height

  def initialize(name, age, email, gender, height)
    @name = name
    @age = age
    @email = email
    @gender = gender
    @height = height
  end
end

This version may be more lines of code than others, but it makes it easier to leverage built-in language features (e.g. default values for arguments or raising errors if initialize is called with incorrect arity).

pje
  • 21,801
  • 10
  • 54
  • 70
  • 4
    Positional parameters can be a nuisance for many reasons - difficult to refactor - and too easy to mismatch variables being passed. – Ross Attrill Jan 07 '14 at 01:13
  • Yeah. I prefer being explicit wherever possible, so I like this approach. It's by far the easiest code to reason about. Positional parameters do make it more difficult to change the signature later, but it's a tradeoff I'm willing to make. – Christopher Davies Dec 10 '15 at 14:43
2

Using all keys from params is not correct, you can define unwilling names. I think it should be kinda white list of names

class Foo
   @@attributes = [:name, :age, :email, :gender, :height]  

   @@attributes.each do |attr|
     class_eval { attr_accessor "#{attr}" }
   end  

   def initialize params
     @@attributes.each do |attr|
       instance_variable_set("@#{attr}", params[attr]) if params[attr]
     end  
   end
end

Foo.new({:name => 'test'}).name #=> 'test'
megas
  • 21,401
  • 12
  • 79
  • 130
  • How would you use `get_attributes` instead of `@@attributes = [:name, :age, :email, :gender, :height]`? They are already defined with `attr_accessor :name, :age, :email, :gender, :height`. – B Seven Oct 07 '12 at 15:42
  • 1
    You could also `attr_accessor *@@attributes`. The whitelisting here makes all kinds of sense. – mu is too short Jan 31 '14 at 19:12
1

If you are receiving a hash as the sole argument, why not just keep that as an instance variable? Whenever you need a value, call it from the hash. You can keep the instance variable name short so that it can be easily called.

class Foo
  attr_reader :p
  def initalize p
    @p = p
  end
  def foo
    do_something_with(@p[:name])
    ...
  end
end

If @p[:name] is still too lengthy for you, then you can save a proc as an instance variable, and call the relevant value like @p.(:name).

class Foo
  attr_reader :p
  def initialize p
    @p = ->x{p[x]}
  end
  def foo
    do_something_with(@p.(:name))
    ...
  end
end

Or, still an alternative way is to define a method that calls the hash and applies the key.

class Foo
  def initalize p
    @p = p
  end
  def get key
    @p[key]
  end
  def foo
    do_something_with(get(:name))
    ...
  end
end

If want to set the values, you can define a setter method, and further check for invalid keys if you want.

class Foo
  Keys = [:name, :age, :email, :gender, :height]
  def initalize p
    raise "Invalid key in argument" unless (p.keys - Keys).empty?
    @p = p
  end
  def set key, value
    raise "Invalid key" unless Keys.key?(key)
    @p[key] = value
  end
  def get key
    @p[key]
  end
  def foo
    do_something_with(get(:name))
    ...
  end
end
sawa
  • 165,429
  • 45
  • 277
  • 381
0

In the spirit of @steenslag proposal, but also extending the class with some method from a module:

module Greeting
  def greet
    puts "Hello, my name is #{name} and I'm #{age} years old."
  end
end

Person = Struct.new(:name, :age) do
  include Greeting
end

person = Person.new("Alice", 25)
person.greet # => Hello, my name is Alice and I'm 25 years old.
psychoslave
  • 2,783
  • 3
  • 27
  • 44