Using C#-style Events In Ruby

16 commentsWritten on August 22nd, 2010 by
Categories: Ruby

I'm still learning Ruby, and am almost through my second book about it. But i finally caved in to the urge to just start playing around with it instead of reading about it first. One of the things i noticed so far about the language, is that it doesn't have something like C#'s events. At least not out of the box. I thought it would be fun to write something that allows me to define and use events in Ruby in a way that is very similar to how it works in C#. It actually is pretty easy to do this and i think it shows some of the power and flexibility of the Ruby language.

For those of you who already know and use Ruby: i know that this is most likely not the best way to do this, and that all of this is probably already available. But keep in mind that this is my 'hello world' in Ruby and that i'm just playing around with it.

First of all, we're going to need an Event class:

class Event

    attr_reader :name

    def initialize(name)
        @name = name
        @handlers = []
    end

    def +(eventhandler)
        raise TypeError "Method expected" unless eventhandler.is_a? Method
        @handlers[@handlers.size] = eventhandler
        self
    end
    
    def -(eventhandler)
        @handlers.delete eventhandler
        self
    end
    
    def trigger(args)
        @handlers.each { |handler| handler.call args }
    end
    
end

This code is really simple. An Event instance has a name, and an array of handlers. A handler is just a reference to a Method that we can execute whenever we want. The + method allows you to add a handler to the event, and it simply returns self, so we can sort of mimic the C# event subscription code. Our Ruby variant basically looks like this:

 publisher.some_event += method(:my_event_handler) 

The - method basically uses the same trick, so unsubscribing from an event looks like this:

 publisher.some_event -= method(:my_event_handler) 

With that in place, we need a way to define an event in a class, and to trigger it. Preferably, this has to look as natural as possible and with that i mean that it should look like it's just supported by language keywords. We naturally can't add language keywords, but we can fake it sort of by adding methods which you can call without parentheses so at least it'll look like language keywords. There are multiple ways to do this, but i've chosen the simplest one, which is to open the Object class and add a few private methods it. Note that in Ruby, private methods can be used by derived classes so these methods are accessible by any class that inherits from it, but you'll never be able to call them on any instance but yourself.

class Object
    
    private
    
    def define_event(symbol)
        getter = symbol
        setter = :"#{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 
        
        define_method setter do |value|
            instance_variable_set variable, value
        end
    end

    def trigger_event(symbol, args)
        event = instance_variable_get :"@#{symbol}"
        event.trigger *args
    end
    
end

This piece of code probably deserves some more explanation :) . We basically add two methods to the Object class: define_event and trigger_event. When define_event is called, we dynamically add 2 methods to the class: a getter and a setter for the newly created Event. The only reason we need the setter is to enable the subscription syntax:

 publisher.some_event += method(:my_event_handler) 

Which is basically the same as doing this:

 publisher.some_event = publisher.some_event + method(:my_event_handler) 

The trigger_event method is very straightforward: it just retrieves the instance variable for the event, and calls its trigger method and passes the args variable.

And that's it... lets demonstrate this new 'language feature' with a simple example. First, we have the Publisher class:

class Publisher
    define_event :notify
    
    def trigger_notify
        trigger_event :notify, "hello world!"
    end
    
end

It defines an event with the name 'notify' and it has a public method to trigger the event. We also have a Subscriber class:

class Subscriber
    
    def start_listening_to(publisher)
        raise TypeError "Publisher expected" unless publisher.is_a? Publisher
        @publisher = publisher
        @publisher.notify += method(:event_handler)
    end

    def stop_listening
        @publisher.notify -= method(:event_handler)
        @publiser = nil
    end
    
    def event_handler(args)
        puts "#{object_id} #{Time.now} received: #{args}"
    end
    
end

As you can see, the Subscriber subscribes and unsubscribes from the 'notify' event in a manner that is very similar to how it's done in C#.

Finally, the output of the following code:

publisher = Publisher.new
subscriber1 = Subscriber.new
subscriber2 = Subscriber.new
subscriber1.start_listening_to publisher
subscriber2.start_listening_to publisher
publisher.trigger_notify
subscriber1.stop_listening
publisher.trigger_notify

is this:

2148074920 Sun Aug 22 12:17:58 +0200 2010 received: hello world! 2148074900 Sun Aug 22 12:17:58 +0200 2010 received: hello world! 2148074900 Sun Aug 22 12:17:58 +0200 2010 received: hello world!

As you can see, subscriber1 received the event once, while subscriber2 received it twice.

Again, this is certainly not the best way to do this but i just wanted to try this because i can. And for the experienced Ruby devs among you, please go easy on me since this is just my first piece of Ruby code :)

  • Scott Lowe

    This is great fun, Davy. I look forward to more of these posts as you journey deeper into Ruby.

    Would it give your API user more flexibility if you used a module to mixin your event publishing powers e.g.:


    module Publishable
    self.class.class_eval do
    def define_event(symbol)

    end
    end

    def trigger_event(symbol, args)

    end
    end

    Then you could have the following only where you want it (rather than on all objects):


    class Publisher
    include Publishable
    define_event :notify

    def trigger_notify
    trigger_event :notify, "hello world!"
    end
    end

  • http://davybrion.com Davy Brion

    That’s what i tried to do at first, but i ran into some problems with calling the private define_method method that way. I probably did something stupid and i tried some other stuff but didn’t quite get that working either, no doubt due to my n00bness :)

    so i just went with adding those methods to the Object class :)

  • Scott Lowe

    Yeah, I had the same problem – Because instance methods are defined on class, hence ‘self.class.class_eval’. I’ve just run your code with this modification and it works. Swapping snippets of code like this actually more enjoyable than reading books, eh?

  • James

    I think Scott is right, I think you still have your C# hat on :)

    A more idiomatic way could be to declare events in the style:

    event :pizza

    Then to subscribe on an instance of the class, pass the handler as a block:

    b = PizzaHut.new
    b.subscribe :pizza { puts “pizza arrived!” }

    It may be a fun exercise to do the meta programming required for this, I know it must be possible though, as Rails does it.

  • Scott Lowe

    Davy, Hope you don’t mind another comment – this is purely because it’s a stimulating discussion, especially as I’m also a C# developer who uses Ruby. This is all in the interests of a healthy debate; I’m not intending to be a negative nit-picker!

    So I’ve been thinking about your line:

    raise TypeError “Publisher expected” unless publisher.is_a? Publisher.

    I understand that you’ve done this to display a helpful exception message, and to guard entry into the method call, but if you think about it, it’s quite C#-ish to care about the type of an object more than what it can do. It would be perhaps more Ruby-ish to take the duck-typing approach and check what the object can do, so you might swap is_a? for respond_to? and write:

    raise NoMethodError.new(“‘notify’ method expected on #{publisher.class}”, :notify) unless publisher.respond_to? :notify

    This produces the following output in a terminal window:

    “`start_listening_to’: notify method expected on Publisher (NoMethodError)”

    However, if you leave out this extra line of check code and simply let the code run with the :notify method missing on your ‘publisher’ parameter, you still get the following output:

    `start_listening_to’: undefined method `notify’ for # (NoMethodError)

    … which is pretty much the same effect, so I’m thinking that there is little to be gained from this type check, and that it would be better to expend extra effort in unit tests. Furthermore, without a type check, your subscriber is free to subscribe to any object that responds to :notify, which is much more flexible and powerful. What do you think?

  • Scott Lowe

    Oops. Second error output was missing the object type:

    `start_listening_to’: undefined method `notify’ for # (NoMethodError)

    I wish we could edit these posts! (Feel free to edit it for me) :-)

  • http://davybrion.com Davy Brion

    @James

    oh i know, i just thought this was a nice example to show the the power and flexibility of the language :)

    as for directly hooking a block to an ‘event’… it sorta depends on what the block has to do… depending on which values (or references…) the block closes on, a Method might sometimes be more correct. Ideally, it should just support both.

    @Scott

    in a way i agree… but then again, this ‘event’ thing isn’t really common and the way the notify method is used is pretty specific. That is, there should be notify accessors, and it should return and take an Event instance. Any other notify method wouldn’t be suitable either, unless the object it returns also defines a + method which does something that is similar enough

    then again, these restrictions might just be an indication that this approach just stinks in Ruby :)

  • Alex Simkin
  • http://davybrion.com Davy Brion

    Alex

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

    James’ suggestion is probably the best, though i’d want to see it work with Methods as well as procs, and a way to unsubscribe… i’m gonna give that a shot soon :)

  • http://nofail.de phoet

    hi davy,

    if you digg deeper into the ruby language, you will see that most patterns you know from c# or java just do not live in a ruby world. the language is so flexible, that you can do it a much simpler way, than you might expect.

    btw, in ruby, you can write stuff like:

    # use << to append stuff
    @handlers << eventhandler

    # returns self, because it's the last element evaluated
    @handlers.delete eventhandler and self

    kind regards,
    peter

  • http://davybrion.com Davy Brion

    @Phoet

    i’ve noticed that about the patterns and approaches :)

    but sometimes, merely for experimental purposes it can be interesting to just get something ‘familiar’ working, even though you’re not intending to make actual use of it

    In the case of these events, i generally make very little use of them in C# (except in Silverlight, but that’s purely because it’s a very event-driven programming model) and i’m not really planning on using this in Ruby either, unless it would actually make sense to do so :)

  • Alex Simkin
  • Pingback: Using More Rubyesq Events In Ruby | The Inquisitive Coder – Davy Brion's Blog

  • http://davybrion.com Davy Brion

    @Alex

    That one has some nice features, but it doesn’t have a separation between subscribing to an event, and firing one. So a subscriber could actually fire an event, which is something i’d rather avoid.

  • Pingback: Using C#-style Events In Python » Perused

  • Pingback: Weekly Link Post 159 « Rhonda Tipton's WebLog