Onfire

Have bubbling events and observers in all your Ruby objects.

Introduction

If you think “bubbling events” sounds awesome and should definitly be used in your project, you’re lame. However, if you answer “yes” to at least one of the following requirements you’re in. If not, go and use Ruby’s great Observable mixin.

Do you…?

  • prefer decoupled systems, where observers don’t wanna know the observed object (as Observable#add_observer requires)?
  • rather intend to observe events, not business objects alone?
  • have a tree-like data structure? Bubbling events only make sense in a hierarchical environment where observers and event sources form a tree.
  • miss a stop! command which prevents the event from further propagation?

Example

Let’s assume you have a set of User objects with roles, like “CEO”, “Manager”, and “Developer”. You just decided to implement some messaging system where developers can complain, managers can ignore, and the CEO is trying to control.


    CEO:             bill
                    |    |
    Managers:   mike      matz
                          |  |
    Developers:        dave  didi  

If dave would complain about a new policy (which implies exclusive usage of Win PCs only) it would bubble up to his manager matz and then to bill, who’d fire dave right away.

As matz somehow likes his developers he would try to prevent his boss bill from overhearing the conversation or make the complainment management-compatible. Good guy matz.

Installation


  

Usage

First, you extend your User class to be “on fire”.


  class User < ...
    include Onfire

As your User objects don’t have a tree structure you implement #parent. That’s the only requirement Onfire has to the class it’s mixed into.

#parent would return the boss object of the asked instance.


  dave.parent      # => matz
  matz.parent      # => bill
  bill.parent	   # => nil

There’s your hierarchical tree structure.

Fireing events

Now dave issues the bad circumstances in his office:


  dave.fire :thatSucks

So far, nothing would happen as no one in the startup is observing that event.

Responding to events

Anyway, a real CEO should respond to complainments from his subordinates.


  bill.on :thatSucks do puts "who's that?" end

Now bill would at least find out somebody’s crying.


  > dave.fire :thatSucks
  => "who's that?"           # by bill

That’s right, the Onfire API is just the two public methods

  • #on for responding to events and
  • #fire for triggering those

Bubbling events

matz being a good manager wants to mediate, so he takes part in the game:


  matz.on :thatSucks do puts "dave, sshhht!" end

Which results in


  > dave.fire :thatSucks
  => "dave, sshhht!"         # by matz
  => "who's that?"           # by bill

Using the Event object

Of course bill wants to find out who’s the subversive element, so he just asks the revealing Event object.


  bill.on :thatSucks do |event| event.source.fire! end

That’s bad for dave, as he’s unemployed now.

Intercepting events

As dave has always been on time, matz just swallows any offending messages for now.


  matz.on :thatSucks do |event| event.stop! end

That leads to an event that’s stopped at matz. It won’t propagate further up to the big boss.


  > dave.fire :thatSucks
  => "dave, sshhht!"         # first, by matz
  => nil                     # second, matz stops the event.

Organic event filtering

What happens if mike wants to be a good manager, too?


  mike.on :thatSucks do puts "take it easy, dude!" end

When dave starts to cry, there’s no mike involved:


  > dave.fire :thatSucks
  => "dave, sshhht!"         # by matz
  => nil

Obviously, the :thatSucks event triggered by dave never passes mike as he is on a completely different tree branch. The event travels from dave to matz up to bill.

That is dead simple, however it is a clear way to observe only particular events. When mike calls #on he limits his attention to events from his branch – his developers – only.

Event source filtering

After a while discontent moves over to didi.


  > didi.fire :thatSucks
  => "dave, sshhht!"         # first, by matz
  => nil

didi is a lamer and matz always prefered working with dave so he changes his tune.


  matz.on :thatSucks do |event| event.stop! if event.source == dave end

That’s unfair!


  > dave.fire :thatSucks
  => "dave, sshhht!"         # by matz
  => nil                     # dave still got a job.
  > didi.fire :thatSucks
  => "fired!"                # didi's event travels up to boss who fires him.

matz is lazy, so he explicity lets Onfire handle the filtering:


  matz.on :thatSucks, :from => dave do |event| event.stop! end

which will result in the same bad outcome for didi.

Responding with instance methods

Nevertheless matz is trying to keep himself clean, so he refactors the handler block to an instance method.


  matz.instance_eval do
    def shield_dave(event)
      event.stop!
    end
  end
  
  matz.on :thatSucks, :from => dave, :do => :shield_dave

Awesome shit!

Who’s using it?

  • Right now, Onfire is used as clean, small event engine in Apotomo, that’s stateful widgets for Ruby and Rails.

License

Copyright © 2010, Nick Sutterer

The MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.