Условная цепочка в рубине
Есть хороший способ условно цепочки методов в Ruby?
То, что я хочу сделать функционально, это
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...
Поэтому в зависимости от ряда условий я хочу выяснить, какие методы вызывать в цепочке методов.
Пока что моя лучшая попытка сделать это "хорошим способом" - это условно построить строку методов и использовать eval
, но наверняка есть лучший, более рубиновый способ?
10 ответов
Вы можете поместить ваши методы в массив, а затем выполнить все в этом массиве
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
выполняет метод с указанным именем. Enumerable#inject
перебирает массив, давая блоку последнее возвращаемое значение и текущий элемент массива.
Если вы хотите, чтобы ваш метод принимал аргументы, вы также можете сделать это следующим образом
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) }
Ты можешь использовать tap
:
my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c}
Использовать
#yield_self
или, начиная с Ruby 2.6,
#then
!
my_object.
then{ |o| a ? o.some_method_because_of_a : o }.
then{ |o| b ? o.some_method_because_of_b : o }.
then{ |o| c ? o.some_method_because_of_c : o }
Пример класса для демонстрации методов цепочки, которые возвращают скопированный экземпляр без изменения вызывающей стороны. Это может быть библиотека, требуемая вашим приложением.
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: это то, что вы хотите добавить в свое приложение, чтобы включить условную цепочку
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: 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) }
Несмотря на то, что метод инъекции является абсолютно допустимым, этот тип использования Enumerable смущает людей и страдает от ограничения невозможности передачи произвольных параметров.
Такой шаблон может быть лучше для этого приложения:
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
Я обнаружил, что это полезно при использовании именованных областей. Например:
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
Вот более функциональный способ программирования.
использование break
чтобы получить tap()
вернуть результат. (кран находится только в рельсах, как упомянуто в другом ответе)
'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"
Я использую этот шаблон:
class A
def some_method_because_of_a
...
return self
end
def some_method_because_of_b
...
return self
end
end
a = A.new
a.some_method_because_of_a().some_method_because_of_b()
Если вы используете Rails, вы можете использовать #try
, Вместо
foo ? (foo.bar ? foo.bar.baz : nil) : nil
записывать:
foo.try(:bar).try(:baz)
или с аргументами:
foo.try(:bar, arg: 3).try(:baz)
Не определено в ванильном рубине, но не так много кода.
Что бы я не дал за CoffeeScript ?.
оператор.
Может быть, ваша ситуация сложнее, чем это, но почему бы и нет
my_object.method_a if a
my_object.method_b if b
my_object.method_c if c
В итоге я написал следующее:
class Object
# A naïve Either implementation.
# Allows for chainable conditions.
# (a -> Bool), Symbol, Symbol, ...Any -> Any
def either(pred, left, right, *args)
cond = case pred
when Symbol
self.send(pred)
when Proc
pred.call
else
pred
end
if cond
self.send right, *args
else
self.send left
end
end
# The up-coming identity method...
def itself
self
end
end
a = []
# => []
a.either(:empty?, :itself, :push, 1)
# => [1]
a.either(:empty?, :itself, :push, 1)
# => [1]
a.either(true, :itself, :push, 2)
# => [1, 2]