Coupling Systems Loosely with Callbacks

Problem

You want to combine different types of objects without hardcoding them full of references to each other.

Solution

Use a callback system, in which objects register code blocks with each other to be executed as needed. An object can call out to its registered callbacks when it needs something, or it can send notification to the callbacks when it does something.

To implement a callback system, write a "register" or "subscribe" method that accepts a code block. Store the registered code blocks as Proc objects in a data structure: probably an array (if you only have one type of callback) or a hash (if you have multiple types). When you need to call the callbacks, iterate over the data structure and call each of the registered code blocks.

Here's a mixin module that gives each instance of a class its own hash of "listener" callback blocks. An outside object can listen for a particular event by calling subscribe with the name of the event and a code block. The dispatcher itself is responsible for calling notify with an appropriate event name at the appropriate time, and the outside object is responsible for passing in the name of the event it wants to "listen" for.

	module EventDispatcher
	 def setup_ 
listeners
	 @event_dispatcher_listeners = {}
	 end

	 def subscribe(event, &callback)
	 (@event_dispatcher_listeners[event] ||= []) << callback
	 end

	 protected
	 def notify(event, *args)
	 if @event_dispatcher_listeners[event]
	 @event_dispatcher_listeners[event].each do |m|
	 m.call(*args) if m.respond_to? :call
	 end
	 end
	 return nil
	 end
	end

Here's a Factory class that keeps a set of listeners. An outside object can choose to be notified every time a Factory object is created, or every time a Factory object produces a widget:

	class Factory
	 include EventDispatcher

	def initialize
	 setup_listeners
	end

	def produce_widget(color)
	 #Widget creation code goes here…
	 notify(:new_widget, color)
	 end
	end

Here's a listener class that's interested in what happens with Factory objects:

	class WidgetCounter
	 def initialize(factory)
	 @counts = Hash.new(0)
	 factory.subscribe(:new_widget) do |color|
	 @counts[color] += 1
	 puts "#{@counts[color]} #{color} widget(s) created since I started watching."
	 end
	 end
	end

Finally, here's the listener in action:

	f1 = Factory.new
	WidgetCounter.new(f1)
	f1.produce_widget("red")
	# 1 red widget(s) created since I started watching.

	f1.produce_widget("green")
	# 1 green widget(s) created since I started watching.

	f1.produce_widget("red")
	# 2 red widget(s) created since I started watching.

	# This won't produce any output, since our listener is listening to
	# another Factory.
	Factory.new.produce_widget("blue")

 

Discussion

Callbacks are an essential technique for making your code extensible. This technique has many names (callbacks, hook methods, plugins, publish/subscribe, etc.) but no matter what terminology is used, it's always the same. One object asks another to call a piece of code (the callback) when some condition is met. This technique works even when the two objects know almost nothing about each other. This makes it ideal for refactoring big, tightly integrated systems into smaller, loosely coupled systems.

In a pure listener system (like the one given in the Solution), the callbacks set up lines of communication that always move from the event dispatcher to the listeners. This is useful when you have a master object (like the Factory), from which numerous lackey objects (like the WidgetCounter) take all their cues.

But in many loosely coupled systems, information moves both ways: the dispatcher calls the callbacks and then uses the return results. Consider the stereotypical web portal: a customizable homepage full of HTML boxes containing sports scores, weather predictions, and so on. Since new boxes are always being added to the system, the core portal software shouldn't have to know anything about a specific box. The boxes should also know as little about the core software as possible, so that changing the core doesn't require a change to all the boxes.

A simple change to the EventDispatcher class makes it possible for the dispatcher to use the return values of the registered callbacks. The original implementation of EventDispatcher#notify called the registered code blocks, but ignored their return value. This version of EvenTDispatcher#notify yields the return values to a block passed in to notify:

	module EventDispatcher
	 def notify(event, *args)
	 if @event_dispatcher_listeners[event]
	 @event_dispatcher_listeners[event].each do |m|
	 yield(m.call(*args)) if m.respond_to? :call
	 end
	 end
	 return nil
	 end
	end

Here's an insultingly simple portal rendering engine. It lets boxes register to be rendered inside an HTML table, on one of two rows on the portal page:

	class Portal
	 include EventDispatcher

	 def initialize
	 setup_listeners
	 end

	 def render
	 puts ''
	 render_block = Proc.new { |box| puts " " }
	 [:row1, :row2].each do |row|
	 puts ' '
	 notify(row, &render_block)
	 puts ' '
	 end
	 puts '
#{box}

' end end

Here's the rendering engine rendering a specific user's portal layout. This user likes to see a stock ticker and a weather report on the left, and a news box on the right. Note that there aren't even any classes for these boxes; they're so simple they can be implemented as anonymous code blocks:

	portal = Portal.new
	portal.subscribe(:row1) { 'Stock Ticker' }
	portal.subscribe(:row1) { 'Weather' }
	portal.subscribe(:row2) { 'Pointless, Trivial News' }
	portal.render # 
	# 

# # # # # # # #

Stock Ticker Weather
Pointless, Trivial News

If you want the registered listeners to be shared across all instances of a class, you can make listeners a class variable, and make subscribe a module method. This is most useful when you want listeners to be notified whenever a new instance of the class is created.


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net