2015-08-26 20 views
11

Sto costruendo un plug-in che consentirà a uno sviluppatore di aggiungere varie funzionalità a una classe con una semplice dichiarazione nella definizione della classe (seguendo il normale modello act_as).Come posso impostare un hook per eseguire il codice alla fine di una definizione di classe Ruby?

Ad esempio, il codice che consumano il plugin potrebbe essere simile

class YourClass 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
end 

La mia domanda si pone perché voglio errore di controllo che il valore previsto per il: specific_method_to_use parametro esiste come metodo, ma il codice modo è tipicamente organizzato e caricato, il metodo non esiste ancora.

Il codice nel mio plug-in appare timidamente in questo modo:

module MyPlugin 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def consumes_my_plugin(options = {}) 
     raise ArgumentError.new("#{options[:specific_method_to_use]} is not defined") if options[:specific_method_to_use].present? && !self.respond_to?(options[:specific_method_to_use]) 
    end 
    end 
end 

Questo potrebbe funzionare:

class YourClass 
    def your_method; true; end 

    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
end 

Ma questo è come la maggior parte delle persone scrivere codice, e non sarebbe:

class YourClass 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 

    def your_method; true; end 
end 

Come posso fallire al tempo di caricamento di YourClass? Lo voglio errore allora, non in fase di esecuzione con un NoMethodError. Posso rinviare l'esecuzione della linea che solleva l'ArgumentError fino a quando l'intera classe non viene caricata, o fare qualcosa di più intelligente per ottenerlo?

+1

può fare la verifica nel punto in cui si sarebbe invocato 'specific_method_to_use' invece di fare il check in tempo di caricamento della lezione? –

+0

: specifico_method_to_use viene richiamato solo in fase di runtime, quindi anche se potrei sicuramente catturare il NoMethodError e fornire un messaggio costruttivo allo sviluppatore attorno ad esso, il bug verrebbe catturato solo in fase di runtime, non al momento del caricamento ... penso che quest'ultimo sia preferibile. – LikeMaBell

+1

Il controllo che stai facendo usando 'self.respond_to?' Non controllerà il metodo di istanza 'YourClass # your_method'; cercherà il metodo singleton/class "YourClass.your_method'. Nella mia risposta l'ho modificato in "self.instance_methods.include?". Sentiti libero di cambiarlo in 'respond_to?'se ho frainteso la tua intenzione. –

risposta

5

Utilizzare TracePoint per tenere traccia quando la classe invia un evento :end.


soluzione specifica per il vostro problema

Ecco come si può adattare TracePoint direttamente nel vostro modulo di pre-esistente.

require 'active_support/all' 

module MyPlugin 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def consumes_my_plugin(**options) 
     m = options[:specific_method_to_use] 

     TracePoint.trace(:end) do |t| 
     break unless self == t.self 

     raise ArgumentError.new("#{m} is not defined") unless instance_methods.include?(m) 

     t.disable 
     end 
    end 
    end 
end 

Gli esempi che seguono dimostrano che funziona come specificato:

# `def` before `consumes`: evaluates without errors 
class MethodBeforePlugin 
    include MyPlugin 
    def your_method; end 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
end 

# `consumes` before `def`: evaluates without errors 
class PluginBeforeMethod 
    include MyPlugin 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
    def your_method; end 
end 

# `consumes` with no `def`: throws ArgumentError at load time 
class PluginWithoutMethod 
    include MyPlugin 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
end 

soluzione generale

Questo modulo vi permetterà di creare un self.finalize richiamata in qualsiasi classe.

module Finalize 
    def self.extended(obj) 
    TracePoint.trace(:end) do |t| 
     if obj == t.self 
     obj.finalize 
     t.disable 
     end 
    end 
    end 
end 

Ora è possibile estendere la classe e definire self.finalize, che si svolgerà non appena la definizione della classe finisce:

class Foo 
    puts "Top of class" 

    extend Finalize 

    def self.finalize 
    puts "Finalizing #{self}" 
    end 

    puts "Bottom of class" 
end 

puts "Outside class" 

# output: 
# Top of class 
# Bottom of class 
# Finalizing Foo 
# Outside class 
+0

Sembra esattamente la risposta che stavo cercando. Grazie. Le mie scuse per non averti assegnato la piena taglia - stavo viaggiando durante il periodo di premiazione, e non mi sembra consentirmi di assegnare l'intero importo. – LikeMaBell