Ruby’s Observable mixin is often characterized as an Event Handler library. In reality, it only provides basic support for “Property Changed” notifications. If an object needs to raise several different types of events, then the Observable mixin is the wrong tool for the job.
Unobservable overcomes the limitations of the Observable mixin by allowing objects to own one or more Event objects.
require 'unobservable' class Button include Unobservable::Support attr_event :clicked def click(x, y) raise_event(:clicked, x, y) end end button = Button.new button.clicked.register {|x, y| puts "You just clicked: #{x} #{y}"} button.click(2, 3)
Now here’s a slightly-longer demonstration of Unobservable. This time, I even included comments!
require 'unobservable' class Button # This will add basic support for Events to this class. As a bonus, it will # also make the attr_event keyword available to us. include Unobservable::Support # The attr_event keyword allows us to declare the Events that are available # to the instances of the Button class. attr_event :clicked, :double_clicked # This method will raise the :clicked event when it is invoked. def click(x, y) raise_event(:clicked, x, y) end # This method will raise the :double_clicked event when it is invoked def double_click(x, y) raise_event(:double_clicked, x, y) end end # This class does not publish any events, so it does not need to include # the Unobserable::Support mixin. class Textbox attr_accessor :text end # Now let's create some instances of these classes button = Button.new textbox = Textbox.new # We want to automatically update the textbox's text whenever we click # the button. So, let's register an event handler: button.clicked.register do |x, y| textbox.text = "You just clicked: #{x} #{y}" end # We want to print the [x, y] coordinates to the console whenever the # button is double-clicked. So, we can register an event handler that # just calls the Kernel#puts method directly. button.double_clicked.register Kernel, :puts # Show time! First, let's print the textbox's text just to verify # that it's currently null: puts "Before Clicking: #{textbox.text}" # Now click the button. This should raise the :clicked event, which # will invoke its event handlers: button.click(2, 3) # As expected, the event handler that we registered to the :clicked # event updated the textbox's text. puts "After Clicking: #{textbox.text}" # Now double-click the button. This should raise the :double_clicked # event, which will invoke its event handlers. As a result, the # coordinates will be printed to the console. button.double_click(15, 2) # We did not register any event handlers that would change the textbox # when the button was double-clicked. Therefore, we should find that # the textbox's text has remained unchanged. puts "After Double-Clicking: #{textbox.text}"
Support for events can be added on a per-class basis by including the Unobservable::Support module in the desired classes. For example:
require 'unobservable' class Button include Unobservable::Support end
Now the Button class, as well as all of its subclasses, will have support for events. Alternatively, we might decide that we’d like to add support for events to EVERY object. This can be achieved as follows:
require 'unobservable' # Add event support to EVERY object class Object include Unobservable::Support end
Once a class has been given support for events, you can declare events using the attr_event keyword. For instance:
require 'unobservable' class Button include Unobservable::Support attr_event :clicked, :double_clicked end
Like its cousins attr_reader and attr_accessor, attr_event does not actually instantiate any fields when it is invoked. Instead, it just declares which events will exist on instances of the class:
x = Button.new y = Button.new # True. x.clicked returns the same Event instance # each time it is invoked x.clicked === x.clicked # False. x and y each have their own instance of # the Event. x.clicked === y.clicked
The attr_event keyword will automatically create a getter property for each event. Therefore, you can access events as if they were regular attributes:
> x = Button.new => #<Button:0x007fa90c0f1e20> > x.clicked => #<Unobservable::Event:0x007fa90c0edeb0 @handlers=[]>
Events can also be retrieved via the Unobservable::Support#event method:
> x.event(:clicked) => #<Unobservable::Event:0x007fa90c0edeb0 @handlers=[]>
You can retrieve a complete listing of the events supported by an object by invoking the Unobserable::Support#events method:
> x.events => [:clicked, :double_clicked]
Event Handlers can be registered to an Event by calling Unobservable::Event#register (or its alias: Unobservable::Event#add ). For convenience, Unobservable provides 3 different ways to specify Event Handlers:
If the Event Handler only needs to be used in one place, then you can specify it as a Block:
b = Button.new # Specify the Event Handler as a Block b.clicked.register {|x, y| puts "You clicked: #{x}, #{y}"}
If you want to reuse the same Event Handler multiple times, then you can specify it as a Proc:
p = Proc.new {|x, y| puts "STOP POKING #{x}, #{y}!!!"} b = Button.new b.clicked.register p b.double_clicked.register p
If you want the Event Handler to call a specific method on an instance of an Object, then you can specify the instance and the name of the method:
class Foo def handle_click(x, y) puts "You clicked: #{x}, #{y}" end end f = Foo.new b = Button.new b.clicked.register f, :handle_click
Copyright © 2012 Brian Lauber. See LICENSE.txt for further details.