Wie implementiere ich einen "Rückruf" in Ruby?


76

Ich bin mir nicht sicher, welche Redewendung für Rückrufe im C-Stil in Ruby am besten geeignet ist - oder ob es etwas noch Besseres gibt (und weniger wie C). In C würde ich so etwas machen wie:

void DoStuff( int parameter, CallbackPtr callback )
{
  // Do stuff
  ...
  // Notify we're done
  callback( status_code )
}

Was ist ein gutes Ruby-Äquivalent? Im Wesentlichen möchte ich eine übergebene Klassenmethode aufrufen, wenn eine bestimmte Bedingung in "DoStuff" erfüllt ist.


Dies könnte nützlich sein: github.com/krisleech/wisper
Kris

Antworten:


97

Das Rubinäquivalent, das nicht idiomatisch ist, wäre:

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

Der idiomatische Ansatz wäre, einen Block anstelle eines Verweises auf eine Methode zu übergeben. Ein Vorteil eines Blocks gegenüber einer freistehenden Methode ist der Kontext - ein Block ist ein Abschluss , sodass er auf Variablen aus dem Bereich verweisen kann, in dem er deklariert wurde. Dies reduziert die Anzahl der Parameter, die do_stuff an den Rückruf übergeben muss. Zum Beispiel:

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

17
Wenn Sie Yield verwenden, benötigen Sie & block nicht in der Argumentliste.
Douglas

41
Ich benutze immer noch gerne die &blockNotation, weil dann klar wird, dass die Methode einen Block nimmt, wenn man nur die erste Zeile der Definition betrachtet.
Patrick Oscity

stimme w / @Douglas Kommentar zu; Das & Block hat mich alle komisch gemacht :(
Gabriel-Kaam

80

Dieser "idiomatische Block" ist ein zentraler Bestandteil des alltäglichen Ruby und wird häufig in Büchern und Tutorials behandelt. Der Ruby-Informationsabschnitt enthält Links zu nützlichen [Online-] Lernressourcen.


Der idiomatische Weg ist, einen Block zu verwenden:

def x(z)
  yield z   # perhaps used in conjunction with #block_given?
end
x(3) {|y| y*y}  # => 9

Oder vielleicht zu einem Proc konvertiert ; hier zeige ich, dass der "Block", der implizit in einen Proc konvertiert wurde &block, nur ein weiterer "aufrufbarer" Wert ist:

def x(z, &block)
  callback = block
  callback.call(z)
end

# look familiar?
x(4) {|y| y * y} # => 16

(Verwenden Sie das obige Formular nur, um den Block-Now-Proc für die spätere Verwendung oder in anderen Sonderfällen zu speichern, da dadurch Overhead- und Syntaxrauschen hinzugefügt wird.)

Ein Lambda kann jedoch genauso einfach verwendet werden (dies ist jedoch nicht idiomatisch):

def x(z,fn)
  fn.call(z)
end

# just use a lambda (closure)
x(5, lambda {|y| y * y}) # => 25

Während die oben genannten Ansätze können alle einpacken „Aufruf einer Methode“ , wie sie Verschlüsse schaffen, gebundene Methoden können auch als First-Class - aufrufbare Objekte behandelt werden:

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

Außerdem ist es manchmal nützlich, die #sendMethode zu verwenden (insbesondere wenn eine Methode namentlich bekannt ist). Hier wird ein Zwischenmethodenobjekt gespeichert, das im letzten Beispiel erstellt wurde. Ruby ist ein System zur Nachrichtenübermittlung:

# Using A from previous
def x(z, a):
  a.__send__(:b, z)
end
x(8, A.new) # => 64

Viel Spaß beim Codieren!


6

Das Thema etwas genauer untersucht und den Code aktualisiert.

Die folgende Version ist ein Versuch, die Technik zu verallgemeinern, obwohl sie äußerst vereinfacht und unvollständig bleibt.

Ich habe die Implementierung von Rückrufen von DataMapper weitgehend gestohlen, Inspiration gefunden, was mir ziemlich vollständig und schön erscheint.

Ich empfehle dringend, einen Blick auf den Code @ http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb zu werfen

Der Versuch, die Funktionalität mit dem Observable-Modul zu reproduzieren, war jedenfalls sehr ansprechend und lehrreich. Ein paar Anmerkungen:

  • Die hinzugefügte Methode scheint erforderlich zu sein, da die ursprünglichen Instanzmethoden zum Zeitpunkt der Registrierung der Rückrufe nicht verfügbar sind
  • Die einschließende Klasse ist sowohl beobachtet als auch selbstbeobachter
  • Das Beispiel ist auf die Instanzmethoden beschränkt und unterstützt keine Blöcke, Argumente usw.

Code:

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

Diese einfache Darstellung der Verwendung von Observable könnte nützlich sein: http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html


Ich hoffe, es macht Ihnen nichts aus, aber ich habe Ihren Code geschrieben und ihn für mein aktuelles Projekt ein wenig überarbeitet. Es ist im Moment simpel, aber los geht's - zögern Sie nicht zu kritisieren / zu verschleiern usw. github.com/davesims/Simple-AOP/blob/master/lib/simple_aop.rb
Dave Sims

Eine Frage, mit der ich gearbeitet habe - warum haben Sie Observable verwendet? Ich hatte einen Konflikt mit dem Methodennamen mit der Klasse, in die ich mich integrieren musste, und das einfache Aufrufen einer Instanzmethode (trigger_callbacks) funktionierte einwandfrei.
Dave Sims

3

Also, das mag sehr "un-rubin" sein, und ich bin kein "professioneller" Ruby-Entwickler. Wenn ihr also klatscht, seid bitte sanft :)

Ruby hat ein eingebautes Modul namens Observer. Ich fand es nicht einfach zu bedienen, aber um fair zu sein, gab ich ihm keine große Chance. In meinen Projekten habe ich meinen eigenen EventHandler-Typ erstellt (ja, ich verwende häufig C #). Hier ist die Grundstruktur:

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

Um dies zu verwenden, mache ich es als schreibgeschütztes Mitglied einer Klasse verfügbar:

class Foo

  attr_reader :some_value_changed

  def initialize
    @some_value_changed = EventHandler.new
  end

end

Kunden der Klasse "Foo" können ein Ereignis wie das folgende abonnieren:

foo.some_value_changed.add_listener(self, lambda { some_func })

Ich bin mir sicher, dass dies kein idiomatischer Ruby ist und ich habe gerade meine C # -Erfahrung in eine neue Sprache gebracht, aber es hat für mich funktioniert.


1

Wenn Sie bereit sind, ActiveSupport (von Rails) zu verwenden, haben Sie eine einfache Implementierung

class ObjectWithCallbackHooks
  include ActiveSupport::Callbacks
  define_callbacks :initialize # Your object supprots an :initialize callback chain

  include ObjectWithCallbackHooks::Plugin 

  def initialize(*)
    run_callbacks(:initialize) do # run `before` callbacks for :initialize
      puts "- initializing" # then run the content of the block
    end # then after_callbacks are ran
  end
end

module ObjectWithCallbackHooks::Plugin
  include ActiveSupport::Concern

  included do 
    # This plugin injects an "after_initialize" callback 
    set_callback :initialize, :after, :initialize_some_plugin
  end
end

0

Ich implementiere häufig Rückrufe in Ruby wie im folgenden Beispiel. Es ist sehr bequem zu bedienen.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.