How do I pass a conditional expression as a parameter in Ruby?
For example this what I am trying to do,
def method_a(condition, params={}, &block)
if condition
meth开发者_如何学运维od_b(params, &block)
else
yield
end
end
and I am trying to call the method like this,
method_a(#{@date > Date.today}, {:param1 => 'value1', :param2 => 'value2'}) do
end
The result is the condition is always evaluated to true. How do I make it work?
Can't you just pass condition
as the result of a conditional evaluation?
method_a(@date > Date.today, {:param1 => 'value1', :param2 => 'value2'}) do
puts "Do stuff"
end
I think the sensible way to do this would be using a Proc or lambda:
def method_a(condition, params={}, &block)
if condition.call
method_b(params, &block)
else
yield
end
end
method_a(lambda { @date > Date.today }, { :param1 => 'value1', :param2 => 'value2' }) do
# ...
end
A lambda is not evaluated until you call call
on it.
Actually, if you didn't have that comment there in the middle of the line, it would pretty much just work:
method_a(@date > Date.today, {:param1 => 'value1', :param2 => 'value2'}) do; end
By the way: if the very last argument to a method is a hash, you can leave off the curly braces, which makes it read almost like Python-style keyword arguments:
method_a(@date > Date.today, :param1 => 'value1', :param2 => 'value2') do; end
If you use Ruby 1.9, and you have a hash where the keys are symbols, you can use the new alternative hash syntax:
method_a(@date > Date.today, {param1: 'value1', param2: 'value2'}) do; end
Combining the two really looks like keyword arguments:
method_a(@date > Date.today, param1: 'value1', param2: 'value2') do; end
In your method_a
, you could greatly improve the readability by using a guard clause instead of the big honking if
expression:
def method_a(condition, params={}, &block)
return method_b(params, &block) if condition
yield
end
Or the other way around, whichever you think reads better:
def method_a(condition, params={}, &block)
return yield unless condition
method_b(params, &block)
end
However, this is a giant code smell. A method should always do one thing, and one thing only. Every method which takes a boolean argument violates this rule, because it pretty much by definition does two things: one thing if the condition is true and a different thing if the condition is false.
In your original code, this is blatantly obvious, because you have the giant if
expression surrounding the entire method and the code in the two branches is completely different. It is even more obvious than that, because the else
branch not only has completely different code than the then
branch, it also completely ignores the arguments that are passed into the method! So, not only does the method behave differently depending on the condition, it even has a different signature!
What you really want to do, is split the method into two methods. The user of method_a
needs to know anyway what the different behavior between the two cases is, and he has to supply the conditional himself. Instead, he could just call the right method in the first place. So, I would split method_a
into two:
def method_one(params={}, &block)
method_b(params, &block)
end
def method_two
yield
end
And the client can decide which one to call:
if @date > Date.today then
method_two(param1: 'value1', param2: 'value2')
else
method_one do
# something
end
end
But, if you look closely at method_one
, you will see that all it does is just forwarding its arguments unmodified to method_b
. So, we can just get rid of method_one
altogether, and have the client call method_b
directly.
The same goes for method_two
: all it does is call the block. The client could have just as well run the code in the first place.
So, now our library code looks like this:
# there is no spoon
That's right! There is no library code left! (Except for method_b
which is not part of your question.)
And the client code looks like this:
if @date > Date.today then
method_b(param1: 'value1', param2: 'value2')
else
# something
end
A very good example of a method that violates this rule, is Module#instance_methods
in the Ruby core library. It tells you all the instance methods defined in a particular module and class and it takes a boolean argument which decides whether or not this list will include the methods inherited from superclasses. Noone can ever remember whether to pass false
or true
. Noone. Jim Weirich uses this example in his talks about good design and he usually asks the audience which is the inherited case and which is the immediate case. Usually, a high percentage get it wrong. Sometimes, the percentage is worse than just flipping a coin!
If you look at the documentation, it is utterly confusing. I can never remember which way around the conditional goes, I always have to look it up in the documentation. Which isn't really that helpful, because the official documentation, which is part of the actual sourcecode of YARV and MRI, has it the wrong way round, too!
精彩评论