Have bubbling events and observers in all your Ruby objects.
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
- prefer decoupled systems, where observers don’t wanna know the observed object (as
- 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?
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.
First, you extend your
User class to be “on fire”.
class User < ... include Onfire
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.
Now dave issues the bad circumstances in his office:
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
#onfor responding to events and
#firefor triggering those
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
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.
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
: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
> 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
Who’s using it?
- Right now, Onfire is used as clean, small event engine in Apotomo, that’s stateful widgets for Ruby and Rails.
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.