Search code examples
rubyeventsmetaprogrammingobserver-pattern

Ruby: automatically wrapping methods in event triggers


Heres what I have/want:

module Observable
  def observers; @observers; end

  def trigger(event, *args)
    good = true
    return good unless (@observers ||= {})[event]
    @obersvers[event].each { |e| good = false and break unless e.call(self, args) }
    good
  end

  def on(event, &block)
    @obersvers ||= {}
    @obersvers[event] ||= []
    @observers[event] << block
  end

end

class Item < Thing
  include Observable
  def pickup(pickuper)
    return unless trigger(:before_pick_up, pickuper)

    pickuper.add_to_pocket self

    trigger(:after_pick_up, pickuper)
  end

  def drop(droper)
    return unless trigger(:before_drop, droper)

    droper.remove_from_pocket self

    trigger(:after_drop, droper)
  end

  # Lots of other methods
end

# How it all should work
Item.new.on(:before_pickup) do |item, pickuper| 
  puts "Hey #{pickuper} thats my #{item}"
  return false # The pickuper never picks up the object
end

While starting on trying to create a game in Ruby, I thought it would be great if it could be based all around Observers and Events. The problem is have to write all of these triggers seems to be a waste, as it seems like a lot of duplicated code. I feel there must be some meta programming method out there to wrap methods with functionality.

Ideal Sceanrio:

class CustomBaseObject
   class << self
     ### Replace with correct meta magic
     def public_method_called(name, *args, &block)
       return unless trigger(:before_+name.to_sym, args)
       yield block
       trigger(:after_+name.to_sym, args)
     end
     ###
   end
end

And then I have all of my object inherit from this Class.

I'm still new to Ruby's more advanced meta programming subjects, so any knowledge about this type of thing would be awesome.


Solution

  • There are a several ways to do it with the help of metaprogramming magic. For example, you can define a method like this:

    def override_public_methods(c)
      c.instance_methods(false).each do |m|
        m = m.to_sym
        c.class_eval %Q{
          alias #{m}_original #{m}
          def #{m}(*args, &block)
            puts "Foo"
            result = #{m}_original(*args, &block)
            puts "Bar"
            result
          end
        }
      end
    end
    
    class CustomBaseObject
      def test(a, &block)
        puts "Test: #{a}"
        yield
      end
    end
    
    override_public_methods(CustomBaseObject)
    
    foo = CustomBaseObject.new
    foo.test(2) { puts 'Block!' }
    # => Foo
         Test: 2
         Block!
         Bar
    

    In this case, you figure out all the required methods defined in the class by using instance_methods and then override them.

    Another way is to use so-called 'hook' methods:

    module Overrideable
      def self.included(c)
        c.instance_methods(false).each do |m|
          m = m.to_sym
          c.class_eval %Q{
            alias #{m}_original #{m}
            def #{m}(*args, &block)
              puts "Foo"
              result = #{m}_original(*args, &block)
              puts "Bar"
              result
            end
          }
        end
      end
    end
    
    class CustomBaseObject
      def test(a, &block)
        puts "Test: #{a}"
        yield
      end
    
      include Overrideable
    end
    

    The included hook, defined in this module, is called when you include that module. This requires that you include the module at the end of the class definition, because included should know about all the already defined methods. I think it's rather ugly :)