Observer Pattern -- generalized implementation

B

Brian Palmer

I know you can emulate sending different messages using the 'observer'
standard library, but I've never felt that approach was natural, and I
haven't found a general-purpose Observer Pattern library that fits me,
so I've been working on my own approach (borrowing heavily from C#).
I'm still pretty ruby-nuby, though, so I'm looking for
suggestions/improvements. Anyone have any thoughts?

The library is used like so:
-----------------------------------------
# a hypothetical text box object
class TextBox
sends "text_changed", "key_pressed"

def initialize txt = ""
@txt = txt
end

def txt= txt
@txt = txt
text_changed
end

def key_press key
key_pressed key
end
end

def my_key_press(k)
puts "KEY: #{k}"
end

box = TextBox::new
box.on_text_changed += proc { puts "Text changed!" }
box.on_key_pressed += method:)my_key_press)
box.txt= "New text!" # => "Text changed!"
box.key_press 'j' # => "KEY: j"
box.on_key_pressed -= method:)my_key_press)
box.key_press 'k' # => Nothing happens
-----------------------------------------

So yeah, very much like C#. You call += and -= to add and remove
observers, respectively. The functions are generated automatically when
you call Module#sends inside a class definition. So you call
sends("text_changed"), and you get on_text_changed and text_changed made
for you. (text_changed should maybe be called notify_text_changed or
similar)

And the (very basic) library:
-----------------------------------------
module Delegate

class Delegate
def initialize
@observers = []
end
def + observer_method
@observers << observer_method
self
end
def - observer_method
@observers.delete observer_method
self
end
def call *the_args
@observers.each { |cb| cb.call(*the_args) }
end
end # class Delegate

class ::Module
def sends *args
args.each { |arg|
class_eval <<-CEEND
def on_#{arg}
@#{arg}_delegate ||= Delegate::new
end
def on_#{arg}= a
self
end
private
def #{arg} *the_args
@#{arg}_delegate ||= Delegate::new
@#{arg}_delegate.call *the_args
end
CEEND
}
end # def sends
end # class ::Module
end # module Delegate
-----------------------------------------

So yeah, the whole "on_#{arg}= a" thing is a hack, but += doesn't behave
as I would expect when used after a method. I would think it would call
the method first, and then call += on its return value. But it calls
the method, calls + on its return value, then calls the method= version
of the method. Not intuitive, at least not for me, but I guess that's
the order of operations. I tried forcing it by putting () after the
method and before +=, but that just caused a syntax error in ruby 1.8.1.

Anybody have any thoughts? How could I improve it/optimize it? C#'s
handling of delegates and events is my favorite part of the languge,
alongside attribute metadata, so I had to have it in ruby. It's also a
huge boost the the readability of GUI code, in my eyes.

-- Brian Palmer
 
D

David A. Black

Hi --

So yeah, the whole "on_#{arg}= a" thing is a hack, but += doesn't
behave as I would expect when used after a method. I would think it
would call the method first, and then call += on its return value.
But it calls the method, calls + on its return value, then calls the
method= version of the method.

The way you're describing can't really work, though; you'd end up with
the problem of altering immutable values (the same thing that has led
Matz not to have a ++ operator):

obj.x = 1
obj.x += 1 # would resolve to: 1 += 1, which is: 1 = 1 + 1

The syntactic sugar transformation (a += b to a = a + b) has to happen
before anything else happens that could turn the lhs into a
non-l-value.


David
 
B

Brian Palmer

David said:
Hi --




The way you're describing can't really work, though; you'd end up with
the problem of altering immutable values (the same thing that has led
Matz not to have a ++ operator):

obj.x = 1
obj.x += 1 # would resolve to: 1 += 1, which is: 1 = 1 + 1

The syntactic sugar transformation (a += b to a = a + b) has to happen
before anything else happens that could turn the lhs into a
non-l-value.


David
That's true. I figured there was a reason for it, just wasn't sure
what. So it couldn't be set up to simply throw a run-time error
exception if you try to alter an immutable value in this way? Or it
could, but it would take a lot of time/make things slower? Just trying
to understand things better...

Brian
 
D

David A. Black

Hi --

That's true. I figured there was a reason for it, just wasn't sure
what. So it couldn't be set up to simply throw a run-time error
exception if you try to alter an immutable value in this way? Or it
could, but it would take a lot of time/make things slower? Just trying
to understand things better...

I guess it's just a case of not making a special case :) a += b is
always parsed as a = a + b, whether it involves local variables or
methods. This also harmonizes, I think, with the goal of the "="
pseudo operator overall, namely to unify the syntax as between
variable assignment and calling of "=" methods as much as possible.

Another thing is that the occasions when it would be either harmful or
useless to call the method first is pretty large. Other than the
1 = 1 + 1 case, consider also:

class A
attr_writer :s
def s
@s.dup
end
end

(Dup'ing an object in a read method is far from unheard-of, as a way
of protecting the actual object from being modified.)

a = A.new
obj.s = "hello"
obj.s += " there"

If the method obj.s is run first, one of two things happens. You'd
actually be doing:

"hello" += "there"

So either it doesn't parse (because "hello" isn't an l-value), or +=
takes on a new (and suspiciously magic) meaning and we end up with
"hello there" -- which then gets thrown away, because we haven't
assigned it to anything. (Remember, the whole point is that we're
*not* calling the A#s= method.)


David
 
J

Joel VanderWerf

Brian said:
I know you can emulate sending different messages using the 'observer'
standard library, but I've never felt that approach was natural, and I
haven't found a general-purpose Observer Pattern library that fits me,
so I've been working on my own approach (borrowing heavily from C#).
I'm still pretty ruby-nuby, though, so I'm looking for
suggestions/improvements. Anyone have any thoughts?

You might find http://redshift.sourceforge.net/observable interesting. I
should have called it "observable attribute", because observations occur
in the context of an attr of an object rather than the whole object.
Assignments to attrs trigger code in observers, if the assigned value
matches a pattern (and is not == the old value).

It was specifically designed for GUI code (Fox in particular). For
instance, a dialog with some controls gets wired up to another window
with display objects so that the controls stay in sync with the objects.

One limitation, in comaprison with your idea: you can't add unlimited
observer code--you can only add one observer per observing object, per
pattern. (Each observable attr has a hash that maps [observer, pattern]
=> proc.) I tend to think this is sensible, but YMMV.

Here's how your example would look with observable attrs:

require 'observable'

class TextBox
extend Observable

observable :text

def initialize txt = ""
self.text = txt
end

def key=(k)
# Assume keypress is an instantaneous event, so don't assign. We
# can still observe the event.
end

observable :key
# Do this after defining #key= so that the no-op behavior above
# is retained. The #key= method defined by observable will first
# call the pre-existing #key= method (no-op, in this case), then
# notify observers.
end

def my_key_press(k)
puts "KEY: #{k}"
end

box = TextBox::new
box.when_text // do puts "Text changed!" end
box.when_key // do |k| my_key_press(k) end
# The pattern matching uses #===, so patterns can be regexes,
# classes, etc.

puts "---"

box.text = "New text!"

puts "---"

box.key = 'j'
box.key = 'j'
p box.key

puts "---"

box.cancel_when_key //, self # self is the observer
box.key = 'k' # no effect

puts "---"

box.when_text /42/ do puts "The answer!" end
box.text = "42"

__END__

Output:

Text changed!
 
B

Brian Palmer

One limitation, in comaprison with your idea: you can't add unlimited
observer code--you can only add one observer per observing object, per
pattern. (Each observable attr has a hash that maps [observer,
pattern] => proc.) I tend to think this is sensible, but YMMV.
seems a reasonable enough limitation :) I tend to think of the Observer
Pattern in terms of what your library calls signal methods rather than
your observable methods, but I think that's just my Java/.NET background
talking. I really like the way you've done it, I may find use for your
library in my own projects. Thank you.
 
J

Joel VanderWerf

Brian said:
One limitation, in comaprison with your idea: you can't add unlimited
observer code--you can only add one observer per observing object, per
pattern. (Each observable attr has a hash that maps [observer,
pattern] => proc.) I tend to think this is sensible, but YMMV.
seems a reasonable enough limitation :) I tend to think of the Observer
Pattern in terms of what your library calls signal methods rather than
your observable methods, but I think that's just my Java/.NET background
talking. I really like the way you've done it, I may find use for your
library in my own projects. Thank you.

Thanks. I've tended to use the signal methods less than the others, but
in your example it would make more sense to use a signal for the
keypress event. Using a signal, there is no need to define the no-op writer.

The main place I've used observable attrs in is FoxTails (on RAA), which
is an add-on to Fox/FXRuby. Observable attrs are the basis for an
alternative to FXDataTargets.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top