unobservable

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.

2-Second Tour

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)

2-Minute Tour

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}"

Usage

Adding Event support to classes

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

Declaring Events

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

Accessing Events

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]

Copyright © 2012 Brian Lauber. See LICENSE.txt for further details.