foreign - ¿Cómo implementar una "devolución de llamada" en Ruby?
rails after_save (6)
A menudo implemento devoluciones de llamada en Ruby como en el siguiente ejemplo. Es muy cómodo de usar.
class Foo
# Declare a callback.
def initialize
callback( :on_die_cast )
end
# Do some stuff.
# The callback event :on_die_cast is triggered.
# The variable "die" is passed to the callback block.
def run
while( true )
die = 1 + rand( 6 )
on_die_cast( die )
sleep( die )
end
end
# A method to define callback methods.
# When the latter is called with a block, it''s saved into a instance variable.
# Else a saved code block is executed.
def callback( *names )
names.each do |name|
eval <<-EOF
@#{name} = false
def #{name}( *args, &block )
if( block )
@#{name} = block
elsif( @#{name} )
@#{name}.call( *args )
end
end
EOF
end
end
end
foo = Foo.new
# What should be done when the callback event is triggered?
foo.on_die_cast do |number|
puts( number )
end
foo.run
No estoy seguro de la mejor expresión idiomática para las llamadas de estilo C en Ruby, o si hay algo aún mejor (y menos como C). En C, haría algo como:
void DoStuff( int parameter, CallbackPtr callback )
{
// Do stuff
...
// Notify we''re done
callback( status_code )
}
¿Cuál es un buen equivalente de Ruby? Básicamente, quiero llamar a un método aprobado en clase, cuando se cumple una determinada condición dentro de "DoStuff"
El equivalente de rubí, que no es idiomático, sería:
def my_callback(a, b, c, status_code)
puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
end
def do_stuff(a, b, c, callback)
sum = a + b + c
callback.call(a, b, c, sum)
end
def main
a = 1
b = 2
c = 3
do_stuff(a, b, c, method(:my_callback))
end
El enfoque idiomático sería pasar un bloque en lugar de una referencia a un método. Una ventaja que tiene un bloque sobre un método independiente es el contexto: un bloque es un closure , por lo que puede hacer referencia a variables del ámbito en el que se declaró. Esto reduce el número de parámetros que do_stuff necesita pasar a la devolución de llamada. Por ejemplo:
def do_stuff(a, b, c, &block)
sum = a + b + c
yield sum
end
def main
a = 1
b = 2
c = 3
do_stuff(a, b, c) { |status_code|
puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
}
end
Exploré el tema un poco más y actualicé el código.
La siguiente versión es un intento de generalizar la técnica, aunque sigue siendo extremadamente simplificada e incompleta.
Robé en gran medida, hem, encontré inspiración en la implementación de callbacks de DataMapper, que me parece bastante completo y hermoso.
Recomiendo echar un vistazo al código @ http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb
De todos modos, tratar de reproducir la funcionalidad utilizando el módulo Observable fue bastante interesante e instructivo. Algunas notas:
- método agregado parece ser necesario porque los métodos de instancia originales no están disponibles en el momento de registrar las devoluciones de llamada
- la clase incluida se hace tanto observada como autoobservadora
- el ejemplo está limitado a los métodos de instancia, no admite bloques, argumentos, etc.
código:
require ''observer''
module SuperSimpleCallbacks
include Observable
def self.included(klass)
klass.extend ClassMethods
klass.initialize_included_features
end
# the observed is made also observer
def initialize
add_observer(self)
end
# TODO: dry
def update(method_name, callback_type) # hook for the observer
case callback_type
when :before then self.class.callbacks[:before][method_name.to_sym].each{|callback| send callback}
when :after then self.class.callbacks[:after][method_name.to_sym].each{|callback| send callback}
end
end
module ClassMethods
def initialize_included_features
@callbacks = Hash.new
@callbacks[:before] = Hash.new{|h,k| h[k] = []}
@callbacks[:after] = @callbacks[:before].clone
class << self
attr_accessor :callbacks
end
end
def method_added(method)
redefine_method(method) if is_a_callback?(method)
end
def is_a_callback?(method)
registered_methods.include?(method)
end
def registered_methods
callbacks.values.map(&:keys).flatten.uniq
end
def store_callbacks(type, method_name, *callback_methods)
callbacks[type.to_sym][method_name.to_sym] += callback_methods.flatten.map(&:to_sym)
end
def before(original_method, *callbacks)
store_callbacks(:before, original_method, *callbacks)
end
def after(original_method, *callbacks)
store_callbacks(:after, original_method, *callbacks)
end
def objectify_and_remove_method(method)
if method_defined?(method.to_sym)
original = instance_method(method.to_sym)
remove_method(method.to_sym)
original
else
nil
end
end
def redefine_method(original_method)
original = objectify_and_remove_method(original_method)
mod = Module.new
mod.class_eval do
define_method(original_method.to_sym) do
changed; notify_observers(original_method, :before)
original.bind(self).call if original
changed; notify_observers(original_method, :after)
end
end
include mod
end
end
end
class MyObservedHouse
include SuperSimpleCallbacks
before :party, [:walk_dinosaure, :prepare, :just_idle]
after :party, [:just_idle, :keep_house, :walk_dinosaure]
before :home_office, [:just_idle, :prepare, :just_idle]
after :home_office, [:just_idle, :walk_dinosaure, :just_idle]
before :second_level, [:party]
def home_office
puts "learning and working with ruby...".upcase
end
def party
puts "having party...".upcase
end
def just_idle
puts "...."
end
def prepare
puts "preparing snacks..."
end
def keep_house
puts "house keeping..."
end
def walk_dinosaure
puts "walking the dinosaure..."
end
def second_level
puts "second level..."
end
end
MyObservedHouse.new.tap do |house|
puts "-------------------------"
puts "-- about calling party --"
puts "-------------------------"
house.party
puts "-------------------------------"
puts "-- about calling home_office --"
puts "-------------------------------"
house.home_office
puts "--------------------------------"
puts "-- about calling second_level --"
puts "--------------------------------"
house.second_level
end
# => ...
# -------------------------
# -- about calling party --
# -------------------------
# walking the dinosaure...
# preparing snacks...
# ....
# HAVING PARTY...
# ....
# house keeping...
# walking the dinosaure...
# -------------------------------
# -- about calling home_office --
# -------------------------------
# ....
# preparing snacks...
# ....
# LEARNING AND WORKING WITH RUBY...
# ....
# walking the dinosaure...
# ....
# --------------------------------
# -- about calling second_level --
# --------------------------------
# walking the dinosaure...
# preparing snacks...
# ....
# HAVING PARTY...
# ....
# house keeping...
# walking the dinosaure...
# second level...
Esta simple presentación del uso de Observable podría ser útil: http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html
Por lo tanto, esto puede ser muy "no ruby", y no soy un desarrollador "profesional" de Ruby, así que si ustedes van a serlo, sean amables por favor :)
Ruby tiene un módulo integrado llamado Observer. No me ha resultado fácil de usar, pero para ser justos, no le di muchas posibilidades. En mis proyectos, he recurrido a la creación de mi propio tipo EventHandler (sí, uso mucho C #). Aquí está la estructura básica:
class EventHandler
def initialize
@client_map = {}
end
def add_listener(id, func)
(@client_map[id.hash] ||= []) << func
end
def remove_listener(id)
return @client_map.delete(id.hash)
end
def alert_listeners(*args)
@client_map.each_value { |v| v.each { |func| func.call(*args) } }
end
end
Entonces, para usar esto lo expongo como un miembro de solo lectura de una clase:
class Foo
attr_reader :some_value_changed
def initialize
@some_value_changed = EventHandler.new
end
end
Los clientes de la clase "Foo" pueden suscribirse a un evento como este:
foo.some_value_changed.add_listener(self, lambda { some_func })
Estoy seguro de que esto no es idiomático, Ruby y yo solo estoy calzando mi experiencia C # en un nuevo idioma, pero me ha funcionado.
Sé que esta es una publicación anterior, pero otros que se encuentran con esto pueden encontrar mi solución útil.
http://chrisshepherddev.blogspot.com/2015/02/callbacks-in-pure-ruby-prepend-over.html
Este "bloque idiomático" es una parte muy importante de Ruby todos los días y se cubre con frecuencia en libros y tutoriales. La sección de información de Ruby proporciona enlaces a recursos útiles de aprendizaje [en línea].
La forma idiomática es usar un bloque:
def x(z)
yield z # perhaps used in conjunction with #block_given?
end
x(3) {|y| y*y} # => 9
O tal vez convertido a un Proc ; Aquí muestro que el "bloque", convertido a un Proc implícitamente con &block
, es solo otro valor "invocable":
def x(z, &block)
callback = block
callback.call(z)
end
# look familiar?
x(4) {|y| y * y} # => 16
(Solo use el formulario anterior para guardar el block-now-Proc para su uso posterior o en otros casos especiales, ya que agrega sobrecarga y ruido de sintaxis).
Sin embargo, una lambda puede usarse con la misma facilidad (pero esto no es idiomático):
def x(z,fn)
fn.call(z)
end
# just use a lambda (closure)
x(5, lambda {|y| y * y}) # => 25
Si bien los enfoques anteriores pueden incluir "llamar a un método", ya que crean cierres, los Methods vinculados también se pueden tratar como objetos invocables de primera clase:
class A
def b(z)
z*z
end
end
callable = A.new.method(:b)
callable.call(6) # => 36
# and since it''s just a value...
def x(z,fn)
fn.call(z)
end
x(7, callable) # => 49
Además, a veces es útil usar el método #send
(en particular, si un método se conoce por su nombre). Aquí guarda un objeto Método intermedio que se creó en el último ejemplo; Ruby es un sistema de transmisión de mensajes:
# Using A from previous
def x(z, a):
a.__send__(:b, z)
end
x(8, A.new) # => 64
Feliz codificación!