How to implement a “callback” in Ruby?

后端 未结 7 1326
臣服心动
臣服心动 2020-12-12 10:00

I\'m not sure of the best idiom for C style call-backs in Ruby - or if there is something even better ( and less like C ). In C, I\'d do something like:

voi         


        
相关标签:
7条回答
  • 2020-12-12 10:26

    I often implement callbacks in Ruby like in the following example. It's very comfortable to use.

    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
    
    0 讨论(0)
  • 2020-12-12 10:31

    The ruby equivalent, which isn't idiomatic, would be:

    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
    

    The idiomatic approach would be to pass a block instead of a reference to a method. One advantage a block has over a freestanding method is context - a block is a closure, so it can refer to variables from the scope in which it was declared. This cuts down on the number of parameters do_stuff needs to pass to the callback. For instance:

    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
    
    0 讨论(0)
  • 2020-12-12 10:31

    I know this is an old post, but others that come across this may find my solution helpful.

    http://chrisshepherddev.blogspot.com/2015/02/callbacks-in-pure-ruby-prepend-over.html

    0 讨论(0)
  • 2020-12-12 10:34

    If you are willing to use ActiveSupport (from Rails), you have a straightforward implementation

    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 讨论(0)
  • 2020-12-12 10:35

    So, this may be very "un-ruby", and I am not a "professional" Ruby developer, so if you guys are going to smack be, be gentle please :)

    Ruby has a built-int module called Observer. I have not found it easy to use, but to be fair I did not give it much of a chance. In my projects I have resorted to creating my own EventHandler type (yes, I use C# a lot). Here is the basic structure:

    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
    

    So, to use this I expose it as a readonly member of a class:

    class Foo
    
      attr_reader :some_value_changed
    
      def initialize
        @some_value_changed = EventHandler.new
      end
    
    end
    

    Clients of the "Foo" class can subscribe to an event like this:

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

    I am sure this is not idiomatic Ruby and I am just shoehorning my C# experience into a new language, but it has worked for me.

    0 讨论(0)
  • 2020-12-12 10:36

    Explored the topic a bit more and updated the code.

    The following version is an attempt to generalize the technique, although remaining extremely simplified and incomplete.

    I largely stole - hem, found inspiration in - the implementation of callbacks of DataMapper, which seems to me quite complete and beatiful.

    I strongly suggest to have a look at the code @ http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb

    Anyway, trying to reproduce the functionality using the Observable module was quite engaging and instructive. A few notes:

    • method added seems to be require because the original instance methods are not available at the moment of registering the callbacks
    • the including class is made both observed and self-observer
    • the example is limited to the instance methods, does not support blocks, args and so on

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

    This simple presentation of the use of Observable could be useful: http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html

    0 讨论(0)
提交回复
热议问题