Eventable
An incredibly simple way to add events to your classes.
Description
Provides an easy to use and understand event model. Other systems did way too much for my needs. I didn't need to monitor network IO ports, I didn't want a central loop that polled IO, I just wanted a simple way to add real, non-polled events to a class and to register other classes to listen for those events.
If you want a simple way to add events to your classes without a bunch of other unrelated IO stuff this is the solution for you. (If you want to monitor IO events check out EventMachine)
You might be saying, "What about Observable? Why not just use that?" The problem with observable is that it saves a reference to the observing object. If you drop and add a bunch of observers without removing them from the observed object you've got the potential for a huge memory leak (depending on the size of your listeners of course).
With Eventable you don't have to worry about memory leaks because Eventable only make a reference to the listening object when it needs to talk to it. When it's done the reference goes out of scope and can be garbage collected.
Eventable will even automatically remove registered listeners when they get garbage collected. You can set up a listener and not worry about removing your event hook yourself; it's done for you.
Eventable also allows for more fine-grain control than Observable. You can register for specific events instead of just saying, "Hey, let me know when anything changes."
Examples
This example shows the basics of using Eventable to add an event to a class and listen for that event. (Without threading it's a bit pointless):
require 'eventable'
class EventClass
include Eventable
# This is all you have to do to add an event (after you include Eventable)
event :stuff_happens
# don't name your method fire_event, that's taken
def do_event
puts "firing :stuff_happens"
# And this is all you have to do to make the event happen
fire_event(:stuff_happens, rand(1000))
end
end
class ListenClass
def stuff_happened(stuff)
puts "stuff happened callback: #{stuff}"
end
end
# Create an instance of a class that has an event
evented = EventClass.new
# Create a class that listens for that event
listener = ListenClass.new
# Register the listener with the instance that will have the event
evented.register_for_event(event: :stuff_happens, listener: listener, callback: :stuff_happened)
# We'll just arbitrarilly fire the event to see how it works
evented.do_event
# Wait just to be sure you see it happen
sleep(1)
=> firing :stuff_happens
=> stuff happened callback: 575
This example shows you how you might actually use it in a multi-threaded environment but is a lot harder to read because of all the debug code:
require 'eventable'
class EventedClass
include Eventable
event :stuff_happens
event :other_stuff_happens
def make_stuff_happen(parent_id)
# You handle concurrency however you want, threads or fibers, up to you.
Thread.new{
puts "firing :stuff_happens"
fire_event(:stuff_happens, {:parent_id=>parent_id, :some_value => rand(1000)})
}
end
def start_other_stuff_happening
Thread.new {
5.times do
sleep(rand(1)+2)
puts "firing :other_stuff_happens"
fire_event(:other_stuff_happens)
end
}
end
end
class ListenerClass
def initialize(some_object)
@some_thing = some_object
@some_thing.register_for_event(event: :stuff_happens, listener: self, callback: :stuff_happened)
end
def do_somestuff(parent_id, times=6)
# I wrapped this in a thread to show it works cross threaded
Thread.new{
id = rand(1000)
times.times do
sleep(rand(2)+1)
puts "[#{parent_id}, #{id}]: do_somestuff"
@some_thing.make_stuff_happen(parent_id)
end
}
end
def stuff_happened(stuff)
splat = stuff
puts "[#{splat[:parent_id]}] stuff_happened callback: #{splat[:some_value]}"
end
def other_stuff_happened
puts "[n/a] same_stuff_happened callback: n/a"
end
end
# Now show it running
evented = EventedClass.new
# You can inject the evented class
listener = ListenerClass.new(evented)
# or attach to events outside of a listener class
evented.register_for_event(event: :other_stuff_happens, listener: listener, callback: :other_stuff_happened)
evented.start_other_stuff_happening
(1..3).each do |index|
listener.do_somestuff(index)
puts "[#{index}] did some stuff, sleeping"
sleep(rand(3)+4)
puts "[#{index}] slept"
end
puts "all done"
Version History
2011.06.10
Ver: 0.1.1
Features: If events fired specifically returns true and returns false if it can't for whatever reason (e.g. no listeners registered).
Fixes:
- Throws error if event is fired and no listeners are registered (Thanks for bug report Benjamin Yu)
- Workaround for RubyGems pre 1.8.3 date bug that locks up all of RubyGems (Thanks again Benjamin Yu)
2011.06.06
Ver: 0.1.0
Went crazy and just completely wrapped all calls that modify or read callbacks cache with an instance mutex.
From what I understand locks are really expensive in Ruby so I'll need to clean this up and do some real performance testing.
Note: Releasing just to stop RubyGems.org from showing last beta instead of newest beta when there are only --pre versions available of a gem. I get why they do it, but it's annoying to have people downloading beta 1 when you're really on beta 2. Plus I need to start using it myself...
2011.06.05
Ver: 0.1.0.beta2
Wrapped #_id2ref call in begin...rescue block.
Added thread synchronization to calls that modify or read callbacks cache.
2011.06.05
Ver: 0.1.0.beta1
Completely redesigned from naive first attempt.
Added features
Now includes RSpecs.
Garbage collection safe:
- Won't keep references to listeners open.
- Automatically removes garbage collected listeners from event notifications.
2011.06.04
Ver: 0.0.1.alpha
Just wrote it as a proof of concept.