Observer Pattern -- generalized implementation

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

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

···

On Wed, 18 Aug 2004, Brian Palmer wrote:

--
David A. Black
dblack@wobblini.net

Brian Palmer wrote:

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'
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!

···

---
Text changed!
---
KEY: j
nil
---
Text changed!
The answer!

David A. Black wrote:

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

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

···

On Wed, 18 Aug 2004, Brian Palmer wrote:

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 :slight_smile: 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.

Hi --

David A. Black wrote:

> 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
>
>
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 :slight_smile: 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

···

On Wed, 18 Aug 2004, Brian Palmer wrote:

> On Wed, 18 Aug 2004, Brian Palmer wrote:

--
David A. Black
dblack@wobblini.net

Brian Palmer wrote:

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 :slight_smile: 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.