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_filter
dopo 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_filter
dopo 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
Questo è semplice e ingegnoso. – Swanand
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). –