2009-11-25 12 views
19

¿Hay alguna manera de encadenar métodos condicionalmente en Ruby?encadenamiento condicional en ruby ​​

Lo que quiero hacer es funcionalmente

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... 

Así que dependiendo de una serie de condiciones que quiero averiguar qué métodos para llamar en la cadena de método.

Hasta ahora mi mejor intento de hacer esto en un "buen camino" es construir condicionalmente la cadena de métodos, y utilizar eval, pero seguro que es un mejor, más rubí, forma?

+1

Me pregunto por qué a muchas personas no les interesa el encadenamiento condicional. Limpiaría bastante el código. – Kelvin

Respuesta

28

se puede poner sus métodos en un arry y luego ejecutar todo en esta matriz

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 ejecuta el método con el nombre dado. Enumerable#inject itera sobre la matriz, mientras proporciona al bloque el último valor devuelto y el elemento de matriz actual.

Si desea que su método para tomar argumentos que también podría hacerlo de esta manera

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) } 
+1

+1 - Esto es muy bueno, no había pensado en usar inyectar – DanSingerman

+0

aunque, ¿puedo usar esto si los métodos necesitan tomar argumentos? – DanSingerman

+2

No creo que esto funcione, ya que el resultado de obj.send reemplaza al acumulador en el ciclo, que probablemente no sea un objeto válido para enviar el método solicitado en la siguiente ejecución. Solución fácil: devuelve explícitamente "obj". – hurikhan77

1

Yo uso este patrón:

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() 
+0

Realmente no veo cómo esto ayuda. ¿Puedes expandirte por favor? – DanSingerman

+0

Cambié mi ejemplo para ilustrar mi idea. ¿O simplemente no entendí tu pregunta y quieres crear una lista de métodos dinámicamente? – demas

+0

Demas probablemente tuvo la intención de insinuar que debe poner la prueba 'if a ...' dentro de 'some_method_because_of_a', luego simplemente llame a toda la cadena y deje que los métodos decidan qué hacer –

1

Tal vez su situación es más complicada que esto, pero por qué no :

my_object.method_a if a 
my_object.method_b if b 
my_object.method_c if c 
+0

my_object.method_a.method_b no es equivalente a my_object.method_a my_object.method_b – DanSingerman

+0

Ah. Creo que estaba pensando más en términos de my_object.method_a !, etc. –

3

Aunque el método de inyección es perfectamente válido, ese tipo de uso Enumerable hace confundir peopl ey sufre la limitación de no poder pasar parámetros arbitrarios.

Un patrón de esta manera puede ser mejor para esta aplicación:

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 

He encontrado que esto es útil cuando se utiliza ámbitos mencionados. Por ejemplo:

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 
+0

filtrar en ámbitos fue exactamente el caso de uso donde se produjo este problema para mí. – DanSingerman

+1

Para aplicar parámetros directamente a las condiciones, el siguiente fragmento puede ser útil: Person.all (: conditions => params.slice (: country,: age)) – hurikhan77

+0

¡Es un truco genial si las cosas se ajustan perfectamente! – tadman

7

Puede utilizar tap: Clase

my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c} 
+0

Eso es rieles, en lugar de rubí vainilla, ¿no es así? – DanSingerman

+1

En realidad, los rieles usan 'return',' tap' es de Ruby 1.8.7 puro y 1.9 – MBO

+0

Brilliant: creo que esta es la mejor manera de lograr lo que quiero. Además, en 1.8.6 puede fácilmente parchearlo para definir el método de toque (que acabo de probar, y parecía funcionar bien) – DanSingerman

2

de ejemplo para demostrar el encadenamiento de métodos que devuelven una instancia de copiado sin modificar la persona que llama. Esto podría ser una lib requerida por su aplicación.

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: esto es lo que desea agregar a su aplicación para permitir el encadenamiento condicional

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 

Ejemplo de uso

# 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) } 
1

Si está utilizando rieles, puede utilizar #try .En lugar de

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

escritura:

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

o, con argumentos:

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

no definidos en el rubí de vainilla, pero isn't a lot of code.

Lo que no daría por el operador de CoffeeScript ?..

+0

Sé que esta es una respuesta anterior a una pregunta anterior , pero Ruby tiene un operador equivalente de "Navegación segura" ahora a partir de Ruby 2.3. Es '& .' (consulte este número de función en el enlace de Ruby: https://bugs.ruby-lang.org/issues/11537) – wspurgin

0

terminé escribiendo lo siguiente:

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] 
1

Aquí está una manera más funcional de programación.

Usa break para obtener tap() para devolver el resultado. (toque solo en rieles como se menciona en la otra respuesta)

'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" 
Cuestiones relacionadas