2010-09-23 12 views
5

Voglio intercettare le chiamate di metodo su una classe Ruby ed essere in grado di fare qualcosa prima e dopo l'effettiva esecuzione del metodo. Ho provato il seguente codice, ma ho ricevuto l'errore:Intercettazione metodo rubino

MethodInterception.rb:16:in before_filter': (eval):2:in alias_method': undefined method say_hello' for class HomeWork' (NameError) from (eval):2:in `before_filter'

Qualcuno può aiutarmi a farlo correttamente?

class MethodInterception 

    def self.before_filter(method) 
    puts "before filter called" 
    method = method.to_s 
    eval_string = " 
     alias_method :old_#{method}, :#{method} 

     def #{method}(*args) 
     puts 'going to call former method' 
     old_#{method}(*args) 
     puts 'former method called' 
     end 
    " 
    puts "going to call #{eval_string}" 
    eval(eval_string) 
    puts "return" 
    end 
end 

class HomeWork < MethodInterception 
    before_filter(:say_hello) 

    def say_hello 
    puts "say hello" 
    end 

end 

risposta

2

Meno codice è stato modificato rispetto all'originale. Ho modificato solo 2 righe.

class MethodInterception 

    def self.before_filter(method) 
    puts "before filter called" 
    method = method.to_s 
    eval_string = " 
     alias_method :old_#{method}, :#{method} 

     def #{method}(*args) 
     puts 'going to call former method' 
     old_#{method}(*args) 
     puts 'former method called' 
     end 
    " 
    puts "going to call #{eval_string}" 
    class_eval(eval_string) # <= modified 
    puts "return" 
    end 
end 

class HomeWork < MethodInterception 

    def say_hello 
    puts "say hello" 
    end 

    before_filter(:say_hello) # <= change the called order 
end 

Questo funziona bene.

HomeWork.new.say_hello 
#=> going to call former method 
#=> say hello 
#=> former method called 
14

Ho appena arrivato fino a questo:

module MethodInterception 
    def method_added(meth) 
    return unless (@intercepted_methods ||= []).include?(meth) && [email protected] 

    @recursing = true # protect against infinite recursion 

    old_meth = instance_method(meth) 
    define_method(meth) do |*args, &block| 
     puts 'before' 
     old_meth.bind(self).call(*args, &block) 
     puts 'after' 
    end 

    @recursing = nil 
    end 

    def before_filter(meth) 
    (@intercepted_methods ||= []) << meth 
    end 
end 

Usalo in questo modo:

class HomeWork 
    extend MethodInterception 

    before_filter(:say_hello) 

    def say_hello 
    puts "say hello" 
    end 
end 

Works:

HomeWork.new.say_hello 
# before 
# say hello 
# after 

Il problema di fondo nel codice era che hai rinominato il metodo nel tuo before_filter metodo, ma poi nel codice del client, hai chiamato before_filter prima che il metodo fosse effettivamente definito, dando così luogo a un tentativo di rinominare un metodo che non esiste.

La soluzione è semplice: non farlo ™!

Bene, ok, forse non è così semplice. È possibile semplicemente forzare i client a chiamare sempre before_filterdopo hanno definito i loro metodi. Tuttavia, si tratta di una cattiva progettazione dell'API.

Quindi, è necessario in qualche modo disporre che il codice rinvii il wrapping del metodo finché non esiste effettivamente. Ed è quello che ho fatto: invece di ridefinire il metodo all'interno del metodo before_filter, registro solo il fatto che debba essere ridefinito in seguito. Quindi, eseguo il effettivo ridefinendo nel gancio method_added.

C'è un piccolo problema in questo, perché se si aggiunge un metodo all'interno di method_added, allora ovviamente verrà richiamato immediatamente e si aggiungerà di nuovo il metodo, che porterà a essere richiamato di nuovo, e così via. Quindi, ho bisogno di evitare la ricorsione.

Si noti che questa soluzione in realtà anche impone un ordinamento sul client: mentre la versione del PO solo opere se si chiama before_filterdopo definire il metodo, la mia versione funziona solo se lo si chiama prima. Tuttavia, è banalmente facile estendere in modo che non soffra di questo problema.

Si noti anche che ho fatto alcuni cambiamenti aggiuntivi che non sono correlate al problema, ma che secondo me sono più Rubyish:

  • utilizzare un mixin invece di una classe: l'ereditarietà è una risorsa molto preziosa in Ruby, perché puoi ereditare solo da una classe. I mixin, tuttavia, sono economici: puoi mescolarne quanti ne vuoi. Inoltre: puoi davvero dire che Homework IS-A MethodInterception?
  • utilizzare Module#define_method anziché eval: eval è il male. 'Nuff ha detto. (Non c'era assolutamente alcun motivo per usare eval in primo luogo, nel codice OP.
  • utilizzare la tecnica di avvolgimento del metodo anziché alias_method: la tecnica di catena alias_method inquina lo spazio dei nomi con i metodi inutili old_foo e old_bar. Mi piace il mio spazio dei nomi pulito.

Ho appena fissato alcune delle limitazioni che ho citato sopra, e ha aggiunto un paio di caratteristiche, ma sono troppo pigro per riscrivere le mie spiegazioni, così ho ripubblicare la versione modificata qui:

module MethodInterception 
    def before_filter(*meths) 
    return @wrap_next_method = true if meths.empty? 
    meths.delete_if {|meth| wrap(meth) if method_defined?(meth) } 
    @intercepted_methods += meths 
    end 

    private 

    def wrap(meth) 
    old_meth = instance_method(meth) 
    define_method(meth) do |*args, &block| 
     puts 'before' 
     old_meth.bind(self).(*args, &block) 
     puts 'after' 
    end 
    end 

    def method_added(meth) 
    return super unless @intercepted_methods.include?(meth) || @wrap_next_method 
    return super if @recursing == meth 

    @recursing = meth # protect against infinite recursion 
    wrap(meth) 
    @recursing = nil 
    @wrap_next_method = false 

    super 
    end 

    def self.extended(klass) 
    klass.instance_variable_set(:@intercepted_methods, []) 
    klass.instance_variable_set(:@recursing, false) 
    klass.instance_variable_set(:@wrap_next_method, false) 
    end 
end 

class HomeWork 
    extend MethodInterception 

    def say_hello 
    puts 'say hello' 
    end 

    before_filter(:say_hello, :say_goodbye) 

    def say_goodbye 
    puts 'say goodbye' 
    end 

    before_filter 
    def say_ahh 
    puts 'ahh' 
    end 
end 

(h = HomeWork.new).say_hello 
h.say_goodbye 
h.say_ahh 
+0

Questo è semplice e ingegnoso. – Swanand

+0

Una nota: alias_method inquina lo spazio dei nomi ma usando alias_method + send si otterrà un'esecuzione più veloce di un riferimento al metodo (circa il 50% più veloce nel mio test). –

0

La soluzione di Jörg W Mittag è molto carina. Se vuoi qualcosa di più robusto (leggi ben testato) la risorsa migliore sarebbe il modulo di callback dei binari.

+1

ha detto che stava usando le rotaie ??! – horseyguy

+0

Conteggio di meno di 50 righe di codice nell'esempio di Jörg (classe Homework inclusa). Sicuramente possiamo elaborare una strategia per testarlo fino a quando non lo consideriamo solido e ben testato. –

+0

@banister: Non so da dove Swanand abbia preso quella nozione pazza. Solo il 98% delle persone ruby ​​usa Rails. –

Problemi correlati