module Observable

Synopsis

require "observable"
class Observed
  extend Observable
  observable :var
end

observed = Observed.new
pattern = Object
observed.when_var pattern do puts "changed" end   # ==> changed
observed.var = 1                                  # ==> changed

private instance methods

Module#observable <var>, ...
Module#observable_state <var>, ...

Defines methods to expose state changes to observers. Each argument <variable> is a name (string, symbol, etc.). Typically, the name will correspond to an instance variable @<variable>, as with attr_accessor.

Adding an observable variable var to a class MyClass (or to a module) is easy:

class MyClass
  extend Observable
  observable :var
end

Four methods are defined for each argument: a reader, a writer, and methods for observers to register and unregister their interest in var. Calling the writer method notifies observers of the new value, if it has changed. Observers register their interest by using the when_var method, and de-register using cancel_when_var.

The observable module method can be safely called more than once, so subclasses don't need to know whether the superclass has called it.

Module#signal <var>, ...
Module#observable_signal <var>, ...

Defines methods to expose signals to observers. Signals are general transient events (or "pulses"), rather than transitions from one state to another. The signal has a value only during notification.

The semantics of observable signals is the same as that of observable states except:

One advantage of signals over methods is that it is easy to use closures (Procs) as the handlers.

generated instance methods

MyClass#var

The reader method returns the value of the variable.

Normally, the reader is the same as the method generated by attr_reader. If a method already exists with the name var, the observable declaration uses the existing method. See the ObservableMethodExample in examples/examples.rb. Note that the observable declaration must come after the definition of the reader for it to be used in this way.

MyClass#var= value

The writer method, as with attr_writer, assigns value to the instance variable. If there is a change, the writer checks if the change is of interest to each observer and, if so, calls the observer's registered code.

If a method already exists with the name var=, the observable declaration uses the existing method. See the ObservableMethodExample in examples/examples.rb. Note that the observable declaration must come after the definition of the writer for it to be used in this way.

By default, the writer is public, but it can of course be made private as with any other method:

private :var=

Note that changing the instance variable directly, as in

@var = ...

does not cause notification.

MyClass#when_var pattern=Object do |value| ... end
MyClass#when_var pattern=Object do |value, old_value| ... end

The registration method takes a pattern (any object) and a block. When the variable's value changes as a result of calling the writer, the pattern is matched against the new value using case semantics (i.e., ===). If the match succeeds, the block is called with the new value as an argument. If the block has a second argument, it is assigned the old value.

The match is also checked at the time of registration (that is, when when_var is called). In this case, old_value is nil.

An observer's behavior can be changed simply by calling when_var again with the same pattern and a different block. (The two blocks, the original and the replacement, must have the same self, or else both blocks apply.)

Note that observable can handle arbitrary cycles of observers. See the CycleExample in examples/examples.rb.

The order in which action clauses happen is not specified.

Note that calling the writer with the current value has no effect--no observers are notified. Notification happens only when there is a change in the value. Hence the following code simply detects all changes:

observed.when_var Object do...end

This is in fact the default value for pattern.

Warning: the #when_* methods only detect changes resulting from calling the writer method, as in obj.var = .... Changes directly to the instance variable do not trigger notification.

Also, changes to the internal state of the object do not trigger notification. For instance,

observed.var = [1,2,3]    # triggers notification
observed.var[1] = 0       # no notification

One way to force notification, is to assign nil to the variable and then reassign the previous value.

observed.var = [1,2,3]    # triggers notification
old_value = observed.var
observed.var = nil        # notification of change to nil
observed.var[1] = 0       # no notification
observed.var = old_value  # notification of change back to old_value

Of course, this will trigger two notifications. It would be possible to add a method, perhaps called var_changed, which can be called after changing an object's internal state, and which would notify observers just once. But there is no way to tell the observer what the old value is, which would break the semantics of any observer of the form

observed.when_var ... do |value, old_value| ... end

It is therefore safer to use two notifications.

MyClass#cancel_when_var pattern, observer

An observer can be removed by calling cancel_when_var with the same pattern and the observer. (The value of observer must be the same as the self for the block in the original when_var call.)

Comparison with Observer pattern

The observable declaration has some differences with the standard Observer pattern in observer.rb:

Version

Observable 0.1

The current version of this software can be found at http://redshift.sourceforge.net/observable .

License

This software is distributed under the Ruby license. See http://www.ruby-lang.org.

Author

Joel VanderWerf, vjoel@users.sourceforge.net