Using More Rubyesq Events In Ruby

13 commentsWritten on August 23rd, 2010 by
Categories: Ruby

In my previous post i showed a way to use events in Ruby in a way that is very similar to how it is in C#. Somebody left a comment saying that i should take off my C# hat, and do it in a more typical Ruby way. I agree with that so i wanted to create an implementation based on his comment. I also wanted to avoid opening up the Object class and requiring you to mix-in a module in order to use the events.

The goal is basically to declare an event like this:

 
class Something
    include EventPublisher
    event :some_event
end

And subscribing to the event would be done like this:

    something.subscribe :some_event, method(:some_handler)

or

    something.subscribe(:some_event) { |args| puts "something happened!" }

and if you subscribed with a method, you should be able to unsubscribe like this:

    something.unsubscribe :some_event, method(:some_handler)

This was again pretty simple to implement, though this implementation is not a robust as it could be (so keep that in mind if you ever decide to use this approach). First of all, we again start off with the Event class, which now looks like this:

class Event
    attr_reader :name
    
    def initialize(name)
        @name = name
        @handlers = []
    end
    
    def add(method=nil, &block)
        @handlers << method if method
        @handlers << block if block
    end
    
    def remove(method)
        @handlers.delete method
    end
    
    def trigger(*args)
        @handlers.each { |handler| handler.call *args }
    end 
end

Nothing special here, except that the add method can accept a Method instance, a block, or both. The default value of the method parameter is nil so you can skip it if you only want to hook a block to the event.

And then we have the EventPublisher module that you can mix-in (more on this in a bit) to your class:

module EventPublisher
    def subscribe(symbol, method=nil, &block)
        event = send(symbol)
        event.add method if method
        event.add block if block
    end

    def unsubscribe(symbol, method)
        event = send(symbol)
        event.remove method
    end
    
    private
    
    def trigger(symbol, *args)
        event = send(symbol)
        event.trigger *args
    end

    self.class.class_eval do
        def event(symbol)
            getter = symbol
            variable = :"@#{symbol}"

            define_method getter do
                event = instance_variable_get variable

                if event == nil
                    event = Event.new(symbol.to_s)
                    instance_variable_set variable, event
                end

                event
            end
        end
    end
end

You might be wondering what the following line does:

        event = send(symbol)

This dynamically calls the method with the given symbol. In our case, that would be the getter method to access the event, which we only create during the registration of an event, so we can't call this method like we'd normally do.

Also note that there is no way to unsubscribe a block from the event. Well, there might be a way but i simply don't know how to do it, since a block is not an object and it has no identity. AFAIK (and again, i'm a ruby n00b) there is no good way to compare blocks, so we can't unsubscribe them from events either. So you really only want to subscribe blocks to an event if you're sure that the event will not be published at a time when you don't want the block to be executed. Also, keep in mind that any variables that the block closes on will be kept in memory, so if your block closes on object references, their instances will be kept in memory until the publisher of the event is garbage collected.

If you need to be sure that you can remove the behavior you've added to an event, subscribe with a Method instance and unsubscribe when you need the added behavior removed again!

Now we can define our Publisher from the previous post like this:

class Publisher
    include EventPublisher
    event :notify
    
    def trigger_notify
        trigger :notify, "hello world!"
    end
end

The EventPublisher module is used as a mixin in the Publisher class. Simply put, that means that the methods defined in EventPublisher are now a part of the Publisher class as well (including their definition), and the nice thing about it is that we didn't have to inherit from a base class to inherit this extra functionality.

We also have the following two classes:

class SubscriberWithMethod
    def start_listening_to(publisher)
        @publisher = publisher
        @publisher.subscribe :notify, method(:notify_handler)
    end
    
    def stop_listening
        @publisher.unsubscribe :notify, method(:notify_handler)
    end
    
    def notify_handler(args)
        puts "#{self.class.to_s} received: #{args}"
    end
end

class SubscriberWithBlock
    def start_listening_to(publisher)
        @publisher = publisher
        @publisher.subscribe(:notify) { |args| puts "block received: #{args}" }
    end
end

The first class subscribes to the event through a Method instance, the second simply assigns a block. As you can see, the first class can unsubscribe from the event by passing the Method instance to the unsubscribe method for that given event.

And if we run the following code:

publisher = Publisher.new
subscriber1 = SubscriberWithMethod.new
subscriber2 = SubscriberWithBlock.new
subscriber1.start_listening_to publisher
subscriber2.start_listening_to publisher
publisher.trigger_notify
subscriber1.stop_listening
publisher.trigger_notify

We get the following output:

SubscriberWithMethod received: hello world! block received: hello world! block received: hello world!

  • Scott Lowe

    It’s really lovely to see the code evolution and differences in approach between the two blog posts.

    I reckon it would be possible to to unsubscribe from blocks, but there would be a trade-off in elegance. Yes blocks are pretty much the only things that aren’t objects, but you can wrap them in an instance of the Proc class.

    So when you subscribe with a block, the subscribe method on EventPublisher could convert the block to a proc and return the proc:


    module EventPublisher
    def subscribe(symbol, method=nil, &block)
    event = send(symbol)
    event.add symbol, method if method

    if block
    event.add symbol, block
    Proc.new(block)
    end
    end


    end

    … so your SubscriberWithBlock could keep a reference to the proc:


    def start_listening_to(publisher)
    @publisher = publisher
    @notify_proc = @publisher.subscribe(:notify) { |args| puts "block received: #{args}" }
    end

    You could then use this reference to unsubscribe later:


    def stop_listening
    @publisher.unsubscribe :notify, @notify_proc
    end

    The trade-off is that keeping the reference slightly defeats the purpose for accepting blocks as well as methods in the first place, doesn’t it? Doh!

    BTW, I would have tried it with a lambda so I wouldn’t have to keep the reference to the proc/block, but I’ve read that you can’t do an equality comparison on lambdas, so we couldn’t find it in your @handlers array.

    This is interesting:

    http://www.skorks.com/2010/05/ruby-procs-and-lambdas-and-the-difference-between-them/

  • http://abstractscience.net aberant

    you should check out the Observable module in the ruby stdlib.

  • http://davybrion.com Davy Brion

    @Aberant

    that implements the ‘classic’ Observer pattern, which isn’t very good when you want to make a distinction between multiple changes/events

  • http://www.slashdotdash.net/ Ben Smith

    I’d recommend you take a look at the Eventful gem on github.

    Eventful is a small extension on top of Ruby’s Observable module that implements named events, block listeners and event bubbling. It allows much more flexible event handling behaviour than is typically allowed by Observable, which requires listeners to be objects that implement update and provides no simple way of calling subsets of observers based on event type.

  • http://davybrion.com Davy Brion

    @Ben

    a problem (at least, IMO) with Eventful’s approach is that subscribing to, and firing of events can be done in the same place:

    w = Watcher.new

    w.on(:filechange) { |watcher, path| puts path }
    w.on(:filedelete) { |watcher, path| puts “#{ watcher } deleted #{ path }” }

    w.fire(:filechange, ‘/path/to/file.txt’)
    w.fire(:filedelete, ‘/tmp/pids/event.pid’)

  • Alex Simkin

    Now you may want to implement autowiring of events, so when you do

    @piblisher.subscribe self

    subscribe introspects class and autowires all methods with names like _handler to corresponding events.

  • Alex Simkin

    …with names like “event”_handler …

  • http://davybrion.com Davy Brion

    @Alex

    heh, that might be a good idea, and it would be fun to write as well :)

  • TDG

    Not to sound like a prick, but i think it’s “rubyesque” not “rubyesq”. :D
    Otherwise, interesting article, keep up the good work!

    • http://davybrion.com Davy Brion

      I googled rubyesq and got a hit, so i thought “alright”. But yeah, i should’ve done a google fight :)

  • Pingback: Auto-Wiring Ruby Events | The Inquisitive Coder – Davy Brion's Blog

  • Pingback: First Experiences With RSpec/BDD | The Inquisitive Coder – Davy Brion's Blog

  • Pingback: ‘eventpublisher’ Gem Now Available | The Inquisitive Coder – Davy Brion's Blog