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
end
Below 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
end
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.
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
end
DRY it up! Move attr_reader to initialize_with :)
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteSee my post from June about a similar feature:
ReplyDeleteCreating a field-initializing 'new' method
I agree it would be a useful feature, but why pass a number to initialize_with? Can't you get the size from the hash args anyway?
Hello Charles,
ReplyDeleteThe 2 is the (fake) employee_id, not the number of args.
Cheers, Jay
Pratik, I guess the reason why Jay didn't move attr_reader to initialize_with is that for the attributes some people may want to use only attr_reader, or only attr_writer, or attr_accessor. initialize_with shouldn't be the one making these decisions for the programmers and forcing attr_reader.
ReplyDelete