In my previous post i showed a way to use events in Ruby in a way that is very similar to how it is in C#. Somebody left a comment saying that i should take off my C# hat, and do it in a more typical Ruby way. I agree with that so i wanted to create an implementation based on his comment. I also wanted to avoid opening up the Object class and requiring you to mix-in a module in order to use the events.
The goal is basically to declare an event like this:
class Something
include EventPublisher
event :some_event
end
And subscribing to the event would be done like this:
something.subscribe :some_event, method(:some_handler)
or
something.subscribe(:some_event) { |args| puts "something happened!" }
and if you subscribed with a method, you should be able to unsubscribe like this:
something.unsubscribe :some_event, method(:some_handler)
This was again pretty simple to implement, though this implementation is not a robust as it could be (so keep that in mind if you ever decide to use this approach). First of all, we again start off with the Event class, which now looks like this:
class Event
attr_reader :name
def initialize(name)
@name = name
@handlers = []
end
def add(method=nil, &block)
@handlers << method if method
@handlers << block if block
end
def remove(method)
@handlers.delete method
end
def trigger(*args)
@handlers.each { |handler| handler.call *args }
end
end
Nothing special here, except that the add method can accept a Method instance, a block, or both. The default value of the method parameter is nil so you can skip it if you only want to hook a block to the event.
And then we have the EventPublisher module that you can mix-in (more on this in a bit) to your class:
module EventPublisher
def subscribe(symbol, method=nil, &block)
event = send(symbol)
event.add method if method
event.add block if block
end
def unsubscribe(symbol, method)
event = send(symbol)
event.remove method
end
private
def trigger(symbol, *args)
event = send(symbol)
event.trigger *args
end
self.class.class_eval do
def event(symbol)
getter = 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
end
end
end
You might be wondering what the following line does:
event = send(symbol)
This dynamically calls the method with the given symbol. In our case, that would be the getter method to access the event, which we only create during the registration of an event, so we can't call this method like we'd normally do.
Also note that there is no way to unsubscribe a block from the event. Well, there might be a way but i simply don't know how to do it, since a block is not an object and it has no identity. AFAIK (and again, i'm a ruby n00b) there is no good way to compare blocks, so we can't unsubscribe them from events either. So you really only want to subscribe blocks to an event if you're sure that the event will not be published at a time when you don't want the block to be executed. Also, keep in mind that any variables that the block closes on will be kept in memory, so if your block closes on object references, their instances will be kept in memory until the publisher of the event is garbage collected.
If you need to be sure that you can remove the behavior you've added to an event, subscribe with a Method instance and unsubscribe when you need the added behavior removed again!
Now we can define our Publisher from the previous post like this:
class Publisher
include EventPublisher
event :notify
def trigger_notify
trigger :notify, "hello world!"
end
end
The EventPublisher module is used as a mixin in the Publisher class. Simply put, that means that the methods defined in EventPublisher are now a part of the Publisher class as well (including their definition), and the nice thing about it is that we didn't have to inherit from a base class to inherit this extra functionality.
We also have the following two classes:
class SubscriberWithMethod
def start_listening_to(publisher)
@publisher = publisher
@publisher.subscribe :notify, method(:notify_handler)
end
def stop_listening
@publisher.unsubscribe :notify, method(:notify_handler)
end
def notify_handler(args)
puts "#{self.class.to_s} received: #{args}"
end
end
class SubscriberWithBlock
def start_listening_to(publisher)
@publisher = publisher
@publisher.subscribe(:notify) { |args| puts "block received: #{args}" }
end
end
The first class subscribes to the event through a Method instance, the second simply assigns a block. As you can see, the first class can unsubscribe from the event by passing the Method instance to the unsubscribe method for that given event.
And if we run the following code:
publisher = Publisher.new subscriber1 = SubscriberWithMethod.new subscriber2 = SubscriberWithBlock.new subscriber1.start_listening_to publisher subscriber2.start_listening_to publisher publisher.trigger_notify subscriber1.stop_listening publisher.trigger_notify
We get the following output:
SubscriberWithMethod received: hello world! block received: hello world! block received: hello world!
Pingback: Auto-Wiring Ruby Events | The Inquisitive Coder – Davy Brion's Blog
Pingback: First Experiences With RSpec/BDD | The Inquisitive Coder – Davy Brion's Blog
Pingback: ‘eventpublisher’ Gem Now Available | The Inquisitive Coder – Davy Brion's Blog