DRY Ruby Initialization with Hash Argument
You don't need the constant, but I don't think you can eliminate symbol-to-string:
class Example
attr_reader :name, :age
def initialize args
args.each do |k,v|
instance_variable_set("@#{k}", v) unless v.nil?
end
end
end
#=> nil
e1 = Example.new :name => 'foo', :age => 33
#=> #<Example:0x3f9a1c @name="foo", @age=33>
e2 = Example.new :name => 'bar'
#=> #<Example:0x3eb15c @name="bar">
e1.name
#=> "foo"
e1.age
#=> 33
e2.name
#=> "bar"
e2.age
#=> nil
BTW, you might take a look (if you haven't already) at the Struct
class generator class, it's somewhat similar to what you are doing, but no hash-type initialization (but I guess it wouldn't be hard to make adequate generator class).
HasProperties
Trying to implement hurikhan's idea, this is what I came to:
module HasProperties
attr_accessor :props
def has_properties *args
@props = args
instance_eval { attr_reader *args }
end
def self.included base
base.extend self
end
def initialize(args)
args.each {|k,v|
instance_variable_set "@#{k}", v if self.class.props.member?(k)
} if args.is_a? Hash
end
end
class Example
include HasProperties
has_properties :foo, :bar
# you'll have to call super if you want custom constructor
def initialize args
super
puts 'init example'
end
end
e = Example.new :foo => 'asd', :bar => 23
p e.foo
#=> "asd"
p e.bar
#=> 23
As I'm not that proficient with metaprogramming, I made the answer community wiki so anyone's free to change the implementation.
Struct.hash_initialized
Expanding on Marc-Andre's answer, here is a generic, Struct
based method to create hash-initialized classes:
class Struct
def self.hash_initialized *params
klass = Class.new(self.new(*params))
klass.class_eval do
define_method(:initialize) do |h|
super(*h.values_at(*params))
end
end
klass
end
end
# create class and give it a list of properties
MyClass = Struct.hash_initialized :name, :age
# initialize an instance with a hash
m = MyClass.new :name => 'asd', :age => 32
p m
#=>#<struct MyClass name="asd", age=32>
Ruby class initialize method with default hash
Keyword arguments were introduced with Ruby 2.0.
The documentation gives details, and the Ruby Rogues podcast back then had an interesting discussion about it.
Note that this applies to any method, not only to initialize
.
Ruby method with argument hash with default values: how to DRY?
This one also only creates the instance variables if they are in your defaults
hash, so that you don't accidentally create/overwrite other instance variables.
I'm assuming you meant to say unless val.nil?
:
def initialize(args={})
defaults = {
:page => DEFAULT_PAGE,
:area => DEFAULT_AREA,
:channel => DEFAULT_CHANNEL
}.merge(args).each do |attr, val|
instance_variable_set("@#{attr}", val) if defaults.has_key?(attr) && (not val.nil?)
end if args
end
Ruby class initialization
I would try using a hash for your constructor like the code below adapted from DRY Ruby Initialization with Hash Argument
class Example
attr_accessor :id, :status, :dateTime
def initialize args
args.each do |k,v|
instance_variable_set("@#{k}", v) unless v.nil?
end
end
end
That way setting each of your properties in the constructor becomes optional. As the instance_variable_set method will set each property if the has contains a value for it.
Which means you could support any number of ways to construct your object. The only downside is you might have to do more nil checking in your code but without more information it is hard to know.
Creating a new Object - Usage Examples
To create a new object with this technique all you need to do is pass in a hash to your initialiser:
my_new_example = Example.new :id => 1, :status => 'live'
#=> #<Example: @id=1, @status='live'>
And its flexible enough to create multiple objects without certain properties with one constructor:
my_second_new_example = Example.new :id => 1
#=> #<Example: @id=1>
my_third_new_example = Example.new :status => 'nonlive', :dateTime => DateTime.new(2001,2,3)
#=> #<Example: @id=1, @dateTime=2001-02-03T00:00:00+00:00>
You can still update your properties once the objects have been created:
my_new_example.id = 24
Shortcut for setting initialize() args to attributes?
Here's how you could do that. One instance variable and an associated read/write accessor will be created for each of initialize
's parameters, with the variable having the same name, preceded by @
, and each instance variable will be assigned the value of the associated parameter.
Code
class MyClass
def initialize(< arbitrary parameters >)
self.class.params.each { |v|
instance_variable_set("@#{v}", instance_eval("#{v}")) }
< other code >
end
@params = instance_method(:initialize).parameters.map(&:last)
@params.each { |p| instance_eval("attr_accessor :#{p}") }
class << self
attr_reader :params
end
< other code >
end
Example
class MyClass
def initialize(a, b, c)
self.class.params.each { |v|
instance_variable_set("@#{v}", instance_eval("#{v}")) }
end
@params = instance_method(:initialize).parameters.map(&:last)
@params.each { |p| instance_eval("attr_accessor :#{p}") }
class << self
attr_reader :params
end
end
MyClass.methods(false)
#=> [:params]
MyClass.instance_methods(false)
#=> [:a, :a=, :b, :b=, :c, :c=]
m = MyClass.new(1,2,3)
m.a #=> 1
m.b #=> 2
m.c #=> 3
m.a = 4
m.a #=> 4
Explanation
When class MyClass
is parsed, the class instance variable @params
is assigned an array whose elements are initialize
's parameters. This is possible because the method initialize
been created when the code beginning @params = ...
is parsed.
The method Method#parameters is used to obtain initialize
's parameters. For the example above,
instance_method(:initialize).parameters
#=> [[:req, :a], [:req, :b], [:req, :c]]
so
@params = instance_method(:initialize).parameters.map(&:last)
#=> [:a, :b, :c]
We then create the read/write accessors:
@params.each { |p| instance_eval("attr_accessor :#{p}") }
and a read accessor for @params
, for use by initialize
:
class << self
attr_reader :params
end
When an instance my_class
of MyClass
is created, the parameter values passed to MyClass.new
are passed to initialize
. initialize
then loops though the class instance variable @params
and sets the value of each instance variable. In this example,
MyClass.new(1,2,3)
invokes initialize(a,b,c)
where
a => 1
b => 2
c => 3
We have:
params = self.class.params
#=> [:a, :b, :c]
params.each { |v| instance_variable_set("@#{v}", instance_eval("#{v}")) }
For the first element of params
(:a
), this is:
instance_variable_set("@a", instance_eval(a) }
which is:
instance_variable_set("@a", 1 }
causing @a
to be assigned 1
.
Note the accessor for @params
is not essential:
class MyClass
def initialize(a, b, c)
self.class.instance_variable_get(:@params).each { |v|
instance_variable_set("@#{v}", instance_eval("#{v}")) }
end
@params = instance_method(:initialize).parameters.map(&:last)
@params.each { |p| instance_eval("attr_accessor :#{p}") }
end
How can I DRY up assertions that a method is called with an expected argument?
It's not really a different option, but you can improve option 2 by extracting a helper method:
context ...
it { should_be_set_to 'red' }
end
context ...
it { should_be_set_to 'green' }
end
def should_be_set_to(color)
pen_double.should_receive(:color=).with color
end
That is DRY, more concise than doing it with a subject block, and can be understood to a degree without reading the helper method. (You can probably come up with a better method name than I can, since you know the domain.)
How to avoid instance variable initializing ugliness
You might want to consider using a Struct
:
class Foo < Struct.new(foo,baz,bar,a,b,c,d)
end
foo = Foo.new(1,2,3,4,5,6,7)
foo.bar #=> 2
No need to define an extra initialize
method at all...
Related Topics
Netbeans and Rails Error: Bin/Ruby: No Such File or Directory -- Script/Rails (Loaderror)
Rvm 'Not Found' After Successful Usage and a Few Days Later
Rufus Scheduler Not Running in Production
Limit Space and Memory Used by Imagemagick
Rails: Specified 'Mysql2' for Database Adapter But the Gem Is Not Loaded
Libmysqlclient.So.15: Cannot Open Shared Object File: No Such File or Directory
Confirmation About Pgrep Returning Itself
Split the String to Get Only the First 5 Characters
How to Update Ruby on Linux (Ubuntu)
Best Practices for New Rails Deployments on Linux
Using Rvm on Ubuntu 12.04 to Use Rails. the Program 'Rails' Is Currently Not Installed
Can't Install Ruby Rvm on Ubuntu 16.04 Due to Gpg Bug