Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save njonsson/1c939aec543f4bdd9d999865e78f5b8e to your computer and use it in GitHub Desktop.
Save njonsson/1c939aec543f4bdd9d999865e78f5b8e to your computer and use it in GitHub Desktop.
Specifying default values with Ruby’s Module#attr_reader and #attr_accessor

[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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment