Auto-Wiring Ruby Events

5 commentsWritten on August 26th, 2010 by
Categories: Ruby

My Rubyesq events received some nice comments in the Ruby Show #130 (fast forward to the 15 minute mark in the audio stream, or the 14 minute mark in the video stream if you wanna see or hear the part about the events), so i figured: why not make them even better?

Alex Simkin had suggested to implement some kind of auto-wiring of events. I thought it would be fun to implement, so i did. Suppose we have the following class:

class Publisher
    include EventPublisher
    event :first_event
    event :second_event
    
    def trigger_events
        trigger :first_event, "first event"
        trigger :second_event, "second event"
    end
end

If we want to subscribe to both events, we'd need to write code like this:

        @publisher.subscribe :first_event, method(:first_event_handler)
        @publisher.subscribe :second_event, method(:second_event_handler)

But it obviously would be much nicer if we could just do something like this:

        @publisher.subscribe_all self

The subscribe_all method could then just look at all the methods that the passed-in instance contains, and it could automatically subscribe each suitable handler (based on a simple convention) to the correct event.

That means we could just have a Subscriber class like this:

class Subscriber
    def initialize(publisher)
        @publisher = publisher
        @publisher.subscribe_all self
    end
    
    def stop_listening
        @publisher.unsubscribe_all self
    end
    
    def first_event_handler(args)
        puts args
    end
    
    def second_event_handler(args)
        puts args
    end
end

This was pretty easy to do actually. Our Event class remains the same, but our EventPublisher module does get a few new methods:

    def each_suitable_handler(subscriber)
        possible_handlers = subscriber.class.instance_methods.select { |name| name =~ /\w_handler/ }
        possible_handlers.each do |method_name|
            event_name = /(?<event_name>.*)_handler/.match(method_name)[:event_name]  
            if EVENTS.include? event_name.to_sym
                yield event_name.to_sym, method_name.to_sym
            end
        end
    end

    def subscribe_all(subscriber)
        each_suitable_handler(subscriber) do |event_symbol, method_symbol|
            subscribe event_symbol, subscriber.method(method_symbol)
        end
    end
    
    def unsubscribe_all(subscriber)
        each_suitable_handler(subscriber) do |event_symbol, method_symbol|
            unsubscribe event_symbol, subscriber.method(method_symbol)
        end
    end

And the method to define an event was slightly modified, so it now looks like this:

    self.class.class_eval do
        EVENTS = []

        def event(symbol)
            getter = symbol
            variable = :"@#{symbol}"
            EVENTS << 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

The only difference here is that we define an EVENTS array constant, and every time an event is defined, we add the symbol of the event to the EVENTS array. I'm not very happy with using an array constant to do this, but it was the only way i found to store the symbol name of each event when it's defined while also being able to access those symbols from within the each_suitable_handler method. Again, i'm new at Ruby so i'm probably missing an easier alternative here.

But with these changes in place, we can now run the following code:

publisher = Publisher.new
subscriber = Subscriber.new(publisher)
publisher.trigger_events
subscriber.stop_listening
publisher.trigger_events

Which produces the expected output:

first event second event

For the 5 people who are going to want to use this: i'm going to create a gem for this soon, and the code will be hosted on GitHub. I just need to learn how to create a gem and learn how Git works first though :)

  • Alex Simkin

    Neat. IMHO – I would make unsubscribe_all to unsubscribe all handlers auto-wired or not. Also, I do not know much about multi-threading in Ruby. Is this thing thread safe or we need use_clone_on_write_to_modify_handlers_so_we_can_continue_to_loop_through_them ;) ?

  • http://davybrion.com Davy Brion

    it’s not threadsafe yet, but i’ll make sure that it is ;)

  • bennyb

    Davy – What PC/IDE do you use for Ruby development? I’m kind of interested but being a Java/C# developer, I can’t seem to find syntax practical.

  • http://davybrion.com Davy Brion

    @Bennyb

    i’m using the excellent TextMate editor with the Ruby bundle on OS X

    if you’re used to real IDE’s, then this is a real adjustment at first… but the weird thing is: everything works so fast that i actually feel like i’m more productive than i am with a real IDE, due to all of the slowness that comes along with that

  • bennyb

    @Davy
    Cool! I will try it this weekend on my iMac (which I barely use).