开发者

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!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜