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.
I did something similar a while back. I would be interested in hearing your thoughts on it...
ReplyDeleteDave,
ReplyDeleteI 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
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.
ReplyDeleteBrian,
ReplyDeleteThe 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.
I played with this idea too sometime ago:
ReplyDeletehttp://datanoise.com/articles/2006/8/30/ruby-tricks
Some of us at Atomic Object have done something similar in rails projects with the injection plugin.
ReplyDeleteIt'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.
Check out the magic_options gem, which provides two ways to splat an options hash into instance variables:
ReplyDeletehttps://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. :-)