[This is a DZone Snippet I created in August 2007. DZone Snippets is now a defunct site, so I’m reposting it here.]
Dependency injection in Ruby is as easy as falling off a log. As Jamis Buck has pointed out, DI is a good thing, but DI frameworks for Ruby are overkill. The language makes them unnecessary.
Here's how to enhance Module#attr_reader and #attr_accessor so that they can receive an options hash for specifying the default value of an attribute.
module AttrWithDefaultExtension
module ClassMethods
def attr_accessor(*args)
attrs, attrs_with_defaults = split_for_last_hash(args)
attrs_with_defaults.each do |name, default|
attr_reader_with_default name, default
attr_writer name
end
super(*attrs)
end
def attr_reader(*args)
attrs, attrs_with_defaults = split_for_last_hash(args)
attrs_with_defaults.each do |name, default|
attr_reader_with_default name, default
end
super(*attrs)
end
private
def attr_reader_with_default(name, default)
define_method(name) do
unless instance_variable_defined?("@#{name}")
default = default.call(self) if default.kind_of?(Proc)
instance_variable_set "@#{name}", default
end
instance_variable_get "@#{name}"
end
end
def split_for_last_hash(args)
if args.last.kind_of?(Hash)
[args[0...-1], args.last]
else
[args, {}]
end
end
end
def self.included(other_module)
other_module.extend ClassMethods
end
end
class Object; include AttrWithDefaultExtension; end
Here are the unit tests. They demonstrate not only the enhanced behavior of #attr_reader and #attr_accessor, but also that the standard behavior remains unbroken.
require 'test/unit'
module AttrWithDefaultExtensionTest
class TwoStandardAttrReaders < Test::Unit::TestCase
attr_reader :foo, :baz
def test_should_not_define_first_instance_variable
assert_equal false, instance_variable_defined?(:@foo)
end
def test_should_return_first_instance_variable_when_sent_first_attr_reader
@foo = 'bar'
assert_equal 'bar', foo
end
def test_should_not_define_second_instance_variable
assert_equal false, instance_variable_defined?(:@baz)
end
def test_should_return_second_instance_variable_when_sent_second_attr_reader
@baz = 'bat'
assert_equal 'bat', baz
end
end
class TwoStandardAttrAccessors < Test::Unit::TestCase
attr_accessor :foo, :baz
def test_should_not_define_first_instance_variable
assert_equal false, instance_variable_defined?(:@foo)
end
def test_should_return_first_instance_variable_when_sent_first_attr_reader
@foo = 'bar'
assert_equal 'bar', foo
end
def test_should_set_first_instance_variable_when_sent_first_attr_writer
self.foo = 'bar'
assert_equal 'bar', @foo
end
def test_should_not_define_second_instance_variable
assert_equal false, instance_variable_defined?(:@baz)
end
def test_should_return_second_instance_variable_when_sent_second_attr_reader
@baz = 'bat'
assert_equal 'bat', baz
end
def test_should_set_second_instance_variable_when_sent_second_attr_writer
self.baz = 'bat'
assert_equal 'bat', @baz
end
end
class TwoStandardAttrReadersAndADefault < Test::Unit::TestCase
attr_reader :foo, :baz, :blit => 'blat'
def test_should_not_define_first_instance_variable
assert_equal false, instance_variable_defined?(:@foo)
end
def test_should_return_first_instance_variable_when_sent_first_attr_reader
@foo = 'bar'
assert_equal 'bar', foo
end
def test_should_not_define_second_instance_variable
assert_equal false, instance_variable_defined?(:@baz)
end
def test_should_return_second_instance_variable_when_sent_second_attr_reader
@baz = 'bat'
assert_equal 'bat', baz
end
def test_should_not_define_third_instance_variable
assert_equal false, instance_variable_defined?(:@blit)
end
def test_should_set_third_instance_variable_when_sent_third_attr_reader
blit
assert_equal 'blat', @blit
end
def test_should_not_set_third_instance_variable_if_already_set_when_sent_third_attr_reader
@blit = 'splat'
assert_equal 'splat', blit
end
end
class TwoStandardAttrAccessorsAndADefault < Test::Unit::TestCase
attr_accessor :foo, :baz, :blit => 'blat'
def test_should_not_define_first_instance_variable
assert_equal false, instance_variable_defined?(:@foo)
end
def test_should_return_first_instance_variable_when_sent_first_attr_reader
@foo = 'bar'
assert_equal 'bar', foo
end
def test_should_set_first_instance_variable_when_sent_first_attr_writer
self.foo = 'bar'
assert_equal 'bar', @foo
end
def test_should_not_define_second_instance_variable
assert_equal false, instance_variable_defined?(:@baz)
end
def test_should_return_second_instance_variable_when_sent_second_attr_reader
@baz = 'bat'
assert_equal 'bat', baz
end
def test_should_set_second_instance_variable_when_sent_second_attr_writer
self.baz = 'bat'
assert_equal 'bat', @baz
end
def test_should_not_define_third_instance_variable
assert_equal false, instance_variable_defined?(:@blit)
end
def test_should_set_third_instance_variable_when_sent_third_attr_reader
blit
assert_equal 'blat', @blit
end
def test_should_not_set_third_instance_variable_if_already_set_before_sent_third_attr_reader
@blit = 'splat'
assert_equal 'splat', blit
end
def test_should_set_third_instance_variable_when_sent_third_attr_writer
self.blit = 'splat'
assert_equal 'splat', @blit
end
end
class AProcDefault < Test::Unit::TestCase
attr_accessor :my_object_id => Proc.new { |obj| obj.object_id }
def test_should_not_define_instance_variable
assert_equal false, instance_variable_defined?(:@my_object_id)
end
def test_should_set_instance_variable_to_result_of_proc_when_sent_attr_reader
my_object_id
assert_equal object_id, @my_object_id
end
def test_should_not_set_instance_variable_if_already_set_before_sent_attr_reader
@my_object_id = 'foo'
assert_equal 'foo', my_object_id
end
def test_should_set_instance_variable_when_sent_attr_writer
self.my_object_id = 'foo'
assert_equal 'foo', @my_object_id
end
end
class AProcWithinAProcDefault < Test::Unit::TestCase
attr_accessor :my_proc => Proc.new { Proc.new { } }
def test_should_not_define_instance_variable
assert_equal false, instance_variable_defined?(:@my_proc)
end
def test_should_set_instance_variable_to_result_of_outer_proc_when_sent_attr_reader
my_proc
assert_kind_of Proc, @my_proc
end
def test_should_not_set_instance_variable_if_already_set_before_sent_attr_reader
@my_proc = 'foo'
assert_equal 'foo', my_proc
end
def test_should_set_instance_variable_when_sent_attr_writer
self.my_proc = 'foo'
assert_equal 'foo', @my_proc
end
end
end