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. :-)