On my current project I found we often duplicated the pattern:
def method_nameI saw this often enough that I decided to create a module to reduce the duplication. The module allows you to specify an attribute that will be initialized on the first execution. Additionally, it allows you to specify what type of class you want initialized and it allows you to provide constructor arguments if necessary.
@method_name ||= Klass.new
end
# module definition*Note: adding the constructor arguments is completely optional and the above code will also work if you change
module AttributeInitializer
def attr_init(name, klass, *ctor_args)
eval "define_method(name) { @#{name} ||= klass.new(*ctor_args) }"
end
def self.append_features(mod)
mod.extend(self)
end
end
# example of usage
class Foo
include AttributeInitializer
attr_init :bar, Array, 1, "some_val"
end
# tests to ensure it works as expected
require 'test/unit'
class AttributeInitTest < Test::Unit::TestCase
def test_same_instance_is_returned
f = Foo.new
assert_equal f.bar, f.bar
end
def test_ctor_args_are_correctly_passed
f = Foo.new
assert_equal ["some_val"], f.bar
end
end
attr_init :bar, Array, 1, "some_val"to
attr_init :bar, Arrayand
assert_equal ["some_val"], f.barto
assert_equal [], f.bar
That's really nice. One thing I noticed is that the eval "..." will be executed every time you call the method, which will be very slow. Instead, if you move the eval outside, like this:
ReplyDeleteeval "define_method(name) { @#{name} ||= klass.new(*ctor_args) }"
then it only gets expanded once, when the method itself is defined.
An alternative impl:
ReplyDeletedef attr_init(name, clazz, *args)
class_eval %{def #{name}
@#{name} ||= #{clazz}.new(#{args.join(',')})
end}
end
eval doesn't seem to get much respect in the ruby community.
This might be better, maybe cause the code's not in a string, so you can get syntax highlighting in your editor, looks a little clearer.
Sweet blog Jay
I like the option to customize behaviour with a block. After all, this isn't Java.
ReplyDeletedef attr_init(name, *ctor_args, &init_block)
init_block ||= proc {|klass, *args| klass.new(*args)}
eval "define_method(name) {@#{name} ||= block[*ctor_args]}
end
Pretend I used 'init_block' throughout, eh?
ReplyDeleteThat's absolutely an option. Interestingly though, I've found that I most often use attr_init for simple things like arrays and strings. The version that lives in both of the last two codebases I worked on don't even allow for constructor args.
ReplyDelete