Dry Ruby Initialization With Hash Argument

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



Leave a reply



Submit