Class: Observatory::Dispatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/observatory/dispatcher.rb

Overview

The Dispatcher is the central repository of all registered observers, and is used by observables to send out signals to these observers.

A dispatcher is not a singleton object, which means you may very well have several dispatcher objects in your program, keeping track of different stacks of observers and observables. Note that this requires you to pass your dispatcher object around using dependency injection.

## For observables

The stack of observers for any given signal is kept in #observers. When using #notify, #notify_until or #filter all observers in the stack will be called.

### Notification methods

Observable objects may use the following methods to trigger their observers:

  • #notify to call all observers.

  • #notify_until to call observers until one stops the chain.

  • #filter to let all observers alter a given value.

## For observers

An object that observes another object is an observer, and it can register itself with the Dispatcher to listen to a signal that observable objects may issue.

An observer may be anything that is callable, but will usually be a method, block or Proc object. You may optionally specify an explicit priority for an observer, to make sure it gets called before or after other observers.

Examples:

Using #connect to register a new observer

class Logger
  def log(event)
    puts "Post published by #{event.observable}"
  end
end
logger = Logger.new
dispatcher.connect('post.publish', logger.method(:log))

Using #disconnect to unregister an observer

dispatcher.disconnect('post.publish', logger.method(:log))

Using #notify to let other objects know something has happened

class Post
  include Observable
  attr_reader :title
  def publish
    notify 'post.publish', :title => title
    # do publication stuff here
  end
end

Using #notify_until to delegate saving a record to another object

class Post
  def save
    notify_until 'post.save', :title => title
  end
end

Using #filter to let observers modify the output of the title attribute

class Post
  def title
    filter('post.title', @title).return_value
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeDispatcher

Returns a new instance of Dispatcher.



76
77
78
# File 'lib/observatory/dispatcher.rb', line 76

def initialize
  @observers = {}
end

Instance Attribute Details

#observersHash (readonly)

A list of all registered observers grouped by signal.

Returns:

  • (Hash)


74
75
76
# File 'lib/observatory/dispatcher.rb', line 74

def observers
  @observers
end

Instance Method Details

#connect(signal, observer, options = {}) ⇒ #call #connect(signal, options = {}, &block) ⇒ #call

Register a observer for a given signal.

Instead of adding a method or Proc object to the stack, you could also use a block. Either the observer argument or the block is required.

Optionally, you could pass in an options hash as the last argument, that can specify an explicit priority. When omitted, an internal counter starting from 1 will be used. To make sure your observer is called last, specify a high, positive number. To make sure your observer is called first, specify a high, negative number.

Examples:

Using a block as an observer

dispatcher.connect('post.publish') do |event|
  puts "Post was published"
end

Using a method as an observer

class Reporter
  def log(event)
    puts "Post published"
  end
end
dispatcher.connect('post.publish', Reporter.new.method(:log))

Determining observer call order using priority

dispatcher.connect('pulp', :priority => 10) do
  puts "I dare you!"
end
dispatcher.connect('pulp', :priority => -10) do
  puts "I double-dare you!"
end
# output when "pulp" is triggered:
"I double-dare you!"
"I dare you!"

Overloads:

  • #connect(signal, observer, options = {}) ⇒ #call

    Parameters:

    • signal (String)

      is the name used by the observable to trigger observers

    • observer (#call)

      is the Proc or method that will react to an event issued by an observable.

    • options (Hash) (defaults to: {})

      is an optional Hash of additional options.

    Options Hash (options):

    • :priority (Fixnum)

      is the priority of this observer in the stack of all observers for this signal. A higher number means lower priority. Negative numbers are allowed.

  • #connect(signal, options = {}, &block) ⇒ #call

    Parameters:

    • signal (String)

      is the name used by the observable to trigger observers

    • options (Hash) (defaults to: {})

      is an optional Hash of additional options.

    Options Hash (options):

    • :priority (Fixnum)

      is the priority of this observer in the stack of all observers for this signal. A higher number means lower priority. Negative numbers are allowed.

Returns:

  • (#call)

    the added observer



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/observatory/dispatcher.rb', line 132

def connect(signal, *args, &block)
  # ugly argument parsing.
  # Make sure that there is either a block given, or that the second argument is
  # something callable. If there is a block given, the second argument, if given,
  # must be a Hash which defaults to an empty Hash. If there is no block given,
  # the third optional argument must be Hash.
  if block_given?
    observer = block
    if args.size == 1 && args.first.is_a?(Hash)
      options = args.first
    elsif args.size == 0
      options = {}
    else
      raise ArgumentError, 'When given a block, #connect only expects a signal and options hash as arguments'
    end
  else
    observer = args.shift
    raise ArgumentError, 'Use a block, method or proc to specify an observer' unless observer.respond_to?(:call)
    if args.any?
      options = args.shift
      raise ArgumentError, '#connect only expects a signal, method and options hash as arguments' unless options.is_a?(Hash) || args.any?
    else
      options = {}
    end
  end

  observer_with_priority = {
    :observer => observer,
    :priority => (options[:priority] || next_internal_priority)
  }

  # Initialize the list of observers for this signal and add this observer
  observers[signal] ||= []
  observers[signal] << observer_with_priority

  # Sort all observers on priority
  observers[signal].sort! do |a,b|
    a[:priority] <=> b[:priority]
  end

  observer
end

#disconnect(signal, observer) ⇒ #call?

Removes an observer from a signal stack, so it no longer gets triggered.

Parameters:

  • signal (String)

    is the name of the stack to remove the observer from.

  • observer (#call)

    is the original observer to remove.

Returns:

  • (#call, nil)

    the removed observer or nil if it could not be found



181
182
183
184
185
186
# File 'lib/observatory/dispatcher.rb', line 181

def disconnect(signal, observer)
  return nil unless observers.key?(signal)
  observers[signal].delete_if do |observer_with_priority|
    observer_with_priority[:observer] == observer
  end
end

#filter(event, value) ⇒ Event

Let all registered observers modify a given value. The observable can then use the Event#return_value to get the filtered result back.

You could use #filter to let observers modify arguments to a method before continuing to work on them (just an example).

Parameters:

Returns:



228
229
230
231
232
233
234
# File 'lib/observatory/dispatcher.rb', line 228

def filter(event, value)
  each(event.signal) do |observer|
    value = observer.call(event, value)
  end
  event.return_value = value
  event
end

#notify(event) ⇒ Event

Send out a signal to all registered observers using a new Event instance. The Event#signal will be used to determine the stack of #observers to use.

Using #notify allows observers to take action at a given time during program execution, such as logging important events.

Parameters:

Returns:



197
198
199
200
201
202
# File 'lib/observatory/dispatcher.rb', line 197

def notify(event)
  each(event.signal) do |observer|
    observer.call(event)
  end
  event
end

#notify_until(event) ⇒ Event

Same as #notify, but halt execution as soon as an observer has indicated it has handled the event by returning a non-falsy value.

An event that was acted upon by an observer will be marked as processed.

Parameters:

Returns:

See Also:



212
213
214
215
216
217
# File 'lib/observatory/dispatcher.rb', line 212

def notify_until(event)
  each(event.signal) do |observer|
    event.process! and break if observer.call(event)
  end
  event
end