Thursday, April 19, 2007

Ruby: Assigning instance variables in a constructor

Every time I assign an instance variable in a constructor I remember that I've been meaning to write something that takes care of it for me.
class DomainObject
attr_reader :arg1, :arg2

def initialize(arg1, arg2)
@arg1, @arg2 = arg1, arg2
end
end
So, without further ado.
class Module
def initializer(*args, &block)
define_method :initialize do |*ctor_args|
ctor_named_args = (ctor_args.last.is_a?(Hash) ? ctor_args.pop : {})
(0..args.size).each do |index|
instance_variable_set("@#{args[index]}", ctor_args[index])
end
ctor_named_args.each_pair do |param_name, param_value|
instance_variable_set("@#{param_name}", param_value)
end
initialize_behavior
end

define_method :initialize_behavior, &block
end
end
The above code allows you to create a constructor that takes arguments in order or from a hash. Here's the tests that demonstrate the behavior of the initializer method.
class ModuleExtensionTest < Test::Unit::TestCase
def test_1st_argument
klass = Class.new do
attr_reader :foo

initializer :foo
end
foo = klass.new('foo')
assert_equal 'foo', foo.foo
end

def test_block_executed
klass = Class.new do
attr_reader :bar

initializer do
@bar = 1
end
end
foo = klass.new
assert_equal 1, foo.bar
end

def test_2nd_argument
klass = Class.new do
attr_reader :foo, :baz

initializer :foo, :baz
end
foo = klass.new('foo', 'baz')
assert_equal 'baz', foo.baz
end

def test_used_hash_to_initialize_attrs
klass = Class.new do
attr_reader :foo, :baz, :cat

initializer :foo, :baz, :cat
end
foo = klass.new(:cat => 'cat', :baz => 2, :foo => 'foo')
assert_equal 'foo', foo.foo
assert_equal 2, foo.baz
assert_equal 'cat', foo.cat
end
end
There are limitations, such as not being able to use default values. But, for 80% of the time, this is exactly what I need.

7 comments:

  1. I did something similar a while back. I would be interested in hearing your thoughts on it...

    ReplyDelete
  2. Dave,
    I think the implementation is good. Perhaps it's even more efficient to to class_eval the way you did since it will only happen once. Mine may be less efficient because I use a block that holds it's context.

    Do you use this in your codebases? I just added it to ours, so I'm curious if you have any lessons learned.

    I do like the ability to use an options hash also though. Long constructors are always a pain for remembering argument ordering.

    Like you, I figured someone did something similar in the past. The easiest way to find out is to write about it. =)

    Cheers, Jay

    ReplyDelete
  3. It isn't clear to me why your initializer method has that popping-off-the-hash code. There isn't an example in your tests for that.

    ReplyDelete
  4. Brian,
    The options pop is for grabbing a hash if one was passed in. When no hash is passed in (first 3 tests) then it uses an empty hash. However, if a hash is passed in (the last test) it initializes the instance variables from the hash.

    ReplyDelete
  5. I played with this idea too sometime ago:

    http://datanoise.com/articles/2006/8/30/ruby-tricks

    ReplyDelete
  6. Some of us at Atomic Object have done something similar in rails projects with the injection plugin.

    It's something we've come up with to let us use simple constructor based dependency injection to put together lots of instances of small and simple classes / components.

    In a class we use a 'constructor' helper that takes a list of names, then generates an initialize method that expects to receive a hash that contains those keys, then sets the values to instance variables with the same names.

    ReplyDelete
  7. Check out the magic_options gem, which provides two ways to splat an options hash into instance variables:

    https://github.com/sheldonh/magic_options

    A mate at work was complaining that it should be easier, so we stopped and made it so, and then it became a purist pursuit. :-)

    ReplyDelete

Note: Only a member of this blog may post a comment.