conditional chaining in ruby

2019-01-22 03:35发布

Is there a good way to chain methods conditionally in Ruby?

What I want to do functionally is

if a && b && c
 my_object.some_method_because_of_a.some_method_because_of_b.some_method_because_of_c
elsif a && b && !c
 my_object.some_method_because_of_a.some_method_because_of_b
elsif a && !b && c
 my_object.some_method_because_of_a.some_method_because_of_c

etc...

So depending on a number of conditions I want to work out what methods to call in the method chain.

So far my best attempt to do this in a "good way" is to conditionally build the string of methods, and use eval, but surely there is a better, more ruby, way?

标签: ruby chaining
9条回答
【Aperson】
2楼-- · 2019-01-22 03:38

If you're using Rails, you can use #try. Instead of

foo ? (foo.bar ? foo.bar.baz : nil) : nil

write:

foo.try(:bar).try(:baz)

or, with arguments:

foo.try(:bar, arg: 3).try(:baz)

Not defined in vanilla ruby, but it isn't a lot of code.

What I wouldn't give for CoffeeScript's ?. operator.

查看更多
贪生不怕死
3楼-- · 2019-01-22 03:39

Although the inject method is perfectly valid, that kind of Enumerable use does confuse people and suffers from the limitation of not being able to pass arbitrary parameters.

A pattern like this may be better for this application:

object = my_object

if (a)
  object = object.method_a(:arg_a)
end

if (b)
  object = object.method_b
end

if (c)
  object = object.method_c('arg_c1', 'arg_c2')
end

I've found this to be useful when using named scopes. For instance:

scope = Person

if (params[:filter_by_age])
  scope = scope.in_age_group(params[:filter_by_age])
end

if (params[:country])
  scope = scope.in_country(params[:country])
end

# Usually a will_paginate-type call is made here, too
@people = scope.all
查看更多
我只想做你的唯一
4楼-- · 2019-01-22 03:44

You can use tap:

my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c}
查看更多
The star\"
5楼-- · 2019-01-22 03:54

Here's a more functional programming way.

Use break in order to get tap() to return the result. (tap is in only in rails as is mentioned in the other answer)

'hey'.tap{ |x| x + " what's" if true }
     .tap{ |x| x + "noooooo" if false }
     .tap{ |x| x + ' up' if true }
# => "hey"

'hey'.tap{ |x| break x + " what's" if true }
     .tap{ |x| break x + "noooooo" if false }
     .tap{ |x| break x + ' up' if true }
# => "hey what's up"
查看更多
小情绪 Triste *
6楼-- · 2019-01-22 03:56

You could put your methods into an arry and then execute everything in this array

l= []
l << :method_a if a
l << :method_b if b
l << :method_c if c

l.inject(object) { |obj, method| obj.send(method) }

Object#send executes the method with the given name. Enumerable#inject iterates over the array, while giving the block the last returned value and the current array item.

If you want your method to take arguments you could also do it this way

l= []
l << [:method_a, arg_a1, arg_a2] if a
l << [:method_b, arg_b1] if b
l << [:method_c, arg_c1, arg_c2, arg_c3] if c

l.inject(object) { |obj, method_and_args| obj.send(*method_and_args) }
查看更多
神经病院院长
7楼-- · 2019-01-22 03:56

Sample class to demonstrate chaining methods that return a copied instance without modifying the caller. This might be a lib required by your app.

class Foo
  attr_accessor :field
    def initialize
      @field=[]
    end
    def dup
      # Note: objects in @field aren't dup'ed!
      super.tap{|e| e.field=e.field.dup }
    end
    def a
      dup.tap{|e| e.field << :a }
    end
    def b
      dup.tap{|e| e.field << :b }
    end
    def c
      dup.tap{|e| e.field << :c }
    end
end

monkeypatch: this is what you want to add to your app to enable conditional chaining

class Object
  # passes self to block and returns result of block.
  # More cumbersome to call than #chain_if, but useful if you want to put
  # complex conditions in the block, or call a different method when your cond is false.
  def chain_block(&block)
    yield self
  end
  # passes self to block
  # bool:
  # if false, returns caller without executing block.
  # if true, return result of block.
  # Useful if your condition is simple, and you want to merely pass along the previous caller in the chain if false.
  def chain_if(bool, &block)
    bool ? yield(self) : self
  end
end

Sample usage

# sample usage: chain_block
>> cond_a, cond_b, cond_c = true, false, true
>> f.chain_block{|e| cond_a ? e.a : e }.chain_block{|e| cond_b ? e.b : e }.chain_block{|e| cond_c ? e.c : e }
=> #<Foo:0x007fe71027ab60 @field=[:a, :c]>
# sample usage: chain_if
>> cond_a, cond_b, cond_c = false, true, false
>> f.chain_if(cond_a, &:a).chain_if(cond_b, &:b).chain_if(cond_c, &:c)
=> #<Foo:0x007fe7106a7e90 @field=[:b]>

# The chain_if call can also allow args
>> obj.chain_if(cond) {|e| e.argified_method(args) }
查看更多
登录 后发表回答