2009-11-25 20 views
19

C'è un buon modo per concatenare i metodi in modo condizionale in Ruby?concatenamento condizionale in rubino

Quello che voglio fare è funzionalmente

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

Quindi, a seconda di una serie di condizioni che voglio capire quali sono i metodi per chiamare nella catena metodo.

Finora il mio miglior tentativo di fare questo in un "buon modo" è di costruire in modo condizionale la stringa di metodi e utilizzare eval, ma sicuramente c'è un modo migliore, più rubino, in modo?

+1

Mi chiedo perché più persone non siano interessate al concatenamento condizionale. Pulirebbe piuttosto il codice. – Kelvin

risposta

28

si potrebbe mettere i vostri metodi in un Arry e quindi eseguire tutto in questo 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 esegue il metodo con il nome dato. Enumerable#inject itera sopra l'array, dando al blocco l'ultimo valore restituito e l'elemento dell'array corrente.

Se si desidera che il metodo per prendere argomenti si potrebbe anche fare in questo modo

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 - Questo è molto buono, non avevo pensato di usare l'iniettore – DanSingerman

+0

sebbene, posso usare questo se i metodi devono prendere gli argomenti? – DanSingerman

+2

Non penso che funzionerà, poiché il risultato di obj.send sostituisce l'accumulatore nel ciclo, che probabilmente non è un oggetto valido per inviare il metodo richiesto alla prossima esecuzione. Soluzione facile: restituire esplicitamente "obj". – hurikhan77

1

Io uso questo schema:

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

Non vedo davvero come questo aiuti. Puoi espandere per favore? – DanSingerman

+0

Ho cambiato il mio esempio per illustrare la mia idea. O semplicemente non ho capito la tua domanda e vuoi creare un elenco di metodi in modo dinamico? – demas

+0

Demas probabilmente voleva dire che dovresti mettere il test "if a ..." dentro "some_method_because_of_a', quindi chiamare l'intera catena e lasciare che i metodi decidano cosa fare –

1

Forse la tua situazione è più complicata di così, ma perché non :

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 non è equivalente a my_object.method_a my_object.method_b – DanSingerman

+0

Ah. Immagino che stavo pensando di più in termini di my_object.method_a !, ecc. Il filtro –

3

Anche se il metodo iniettare è perfettamente valida, che tipo di utilizzo Enumerable fa confondere peopl e soffre della limitazione di non essere in grado di passare parametri arbitrari.

Un modello come questo può essere meglio per questa applicazione:

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 

ho trovato questo per essere utile quando si utilizza ambiti nome. Per esempio:

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

sugli ambiti era esattamente il caso d'uso in cui questo problema si è verificato per me. – DanSingerman

+1

Per applicare direttamente i parametri alle condizioni, il seguente frammento può essere utile: Person.all (: conditions => params.slice (: country,: age)) – hurikhan77

+0

Questo è un trucco perfetto se le cose si adattano perfettamente! – tadman

7

È possibile utilizzare tap: classe

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

Questo è rotaie, piuttosto che rubino di vaniglia però non è vero? – DanSingerman

+1

In realtà le rotaie usano 'return',' tap' è di puro Ruby 1.8.7 e 1.9 – MBO

+0

Brillante - Penso che questo sia il modo migliore per ottenere ciò che voglio. Inoltre in 1.8.6 puoi facilmente eseguire il patch della patch per definire il metodo tap (che ho appena provato, e mi sembra che funzioni bene) – DanSingerman

2

esempio per illustrare il concatenamento metodi che restituiscono un'istanza copiato senza modificare il chiamante. Potrebbe essere una lib richiesta dalla tua 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: questo è ciò che si desidera aggiungere alla vostra applicazione per consentire il concatenamento condizionale

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 

utilizzo Esempio

# 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

Se stai usando Rails, è possibile utilizzare #try .Invece di

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

scrittura:

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

o, con argomenti:

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

Non definito in Ruby vaniglia, ma isn't a lot of code.

Cosa non darei per l'operatore ?. di CoffeeScript.

+0

So che questa è una vecchia risposta per una vecchia domanda , ma Ruby ha un equivalente operatore di "Navigazione sicura" ora a partire da Ruby 2.3! È '& .' (fai riferimento a questo problema nel trunk Ruby: https://bugs.ruby-lang.org/issues/11537) – wspurgin

0

ho finito per scrivere la seguente:

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

Ecco un modo di programmazione più funzionale.

Utilizzare break per ottenere tap() per restituire il risultato. (toccare è solo su binari come menzionato nell'altra risposta)

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