Saturday, August 18, 2007
Ruby: initialize_with
In April I wrote about assigning instance variables in a constructor. The code I provided with that entry supports initializing your classes positionally or with a hash.
Since then, I've been thinking that a nice alternative might be a constructor that takes one argument without a name, but any additional arguments must be named (via a hash). Yes, I basically stole this concept from Smalltalk.
Below is a solution to making the above code execute as expected.
One other small change that you might notice is that I've made all the arguments required. That's a preference that I might blog about later, but I think it should be easy enough to remove the raises if you want your options arguments to be optional.
For those interested, here's the code with tests.
Since then, I've been thinking that a nice alternative might be a constructor that takes one argument without a name, but any additional arguments must be named (via a hash). Yes, I basically stole this concept from Smalltalk.
initialize_with :employee_id, :first_name, :last_name
attr_reader :employee_id, :first_name, :last_name
end
initialize_with :amount
attr_reader :amount
endBelow is a solution to making the above code execute as expected.
first_arg = args.shift
define_method :initialize do |*arg|
instance_variable_set "@", arg.shift
required_args = args.inject(first_arg.to_s) {|result, attribute| result << ", " }
raise ArgumentError.new("initialize requires ") if args.any? && arg.empty?
args.each do |attribute|
raise ArgumentError.new("initialize requires ") unless arg.first.has_key?(attribute)
instance_variable_set "@", arg.first[attribute]
end
end
end
endOne other small change that you might notice is that I've made all the arguments required. That's a preference that I might blog about later, but I think it should be easy enough to remove the raises if you want your options arguments to be optional.
For those interested, here's the code with tests.
first_arg = args.shift
define_method :initialize do |*arg|
instance_variable_set "@", arg.shift
required_args = args.inject(first_arg.to_s) {|result, attribute| result << ", " }
raise ArgumentError.new("initialize requires ") if args.any? && arg.empty?
args.each do |attribute|
raise ArgumentError.new("initialize requires ") unless arg.first.has_key?(attribute)
instance_variable_set "@", arg.first[attribute]
end
end
end
end
initialize_with :employee_id, :first_name, :last_name
attr_reader :employee_id, :first_name, :last_name
end
initialize_with :amount
attr_reader :amount
end
unit_tests do
test "verify Person requires all options" do
assert_raises ArgumentError do
Person.new(2, :first_name => 'mike')
end
end
test "verify Person requires first arg" do
assert_raises ArgumentError do
Person.new(:first_name => 'mike', :last_name => 'ward')
end
end
test "verify Person employee_id" do
mike = Person.new(2, :first_name => 'mike', :last_name => 'ward')
assert_equal 2, mike.employee_id
end
test "verify Person first_name" do
mike = Person.new(2, :first_name => 'mike', :last_name => 'ward')
assert_equal 'mike', mike.first_name
end
test "verify Person last_name" do
mike = Person.new(2, :first_name => 'mike', :last_name => 'ward')
assert_equal 'ward', mike.last_name
end
test "verify Money amount" do
money = Money.new(10)
assert_equal 10, money.amount
end
endLabels: initialize_with, initializer, ruby
Thursday, April 19, 2007
Ruby: Assigning instance variables in a constructor
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 DomainObjectSo, without further ado.
attr_reader :arg1, :arg2
def initialize(arg1, arg2)
@arg1, @arg2 = arg1, arg2
end
end
class ModuleThe 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.
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
class ModuleExtensionTest < Test::Unit::TestCaseThere are limitations, such as not being able to use default values. But, for 80% of the time, this is exactly what I need.
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
Labels: initializer, metaprogramming, ruby




