Observer Pattern -- generalized implementation

Discussion in 'Ruby' started by Brian Palmer, Aug 18, 2004.

  1. Brian Palmer

    Brian Palmer Guest

    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
    Brian Palmer, Aug 18, 2004
    #1
    1. Advertising

  2. Hi --

    On Wed, 18 Aug 2004, Brian Palmer wrote:

    > 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

    --
    David A. Black
    David A. Black, Aug 18, 2004
    #2
    1. Advertising

  3. Brian Palmer

    Brian Palmer Guest

    David A. Black wrote:

    > Hi --
    >
    > On Wed, 18 Aug 2004, Brian Palmer wrote:
    >
    >
    >
    >> 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
    Brian Palmer, Aug 18, 2004
    #3
  4. Hi --

    On Wed, 18 Aug 2004, Brian Palmer wrote:

    > David A. Black wrote:
    >
    > > Hi --
    > >
    > > On Wed, 18 Aug 2004, Brian Palmer wrote:
    > >
    > >
    > >
    > >> 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 :) 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

    --
    David A. Black
    David A. Black, Aug 18, 2004
    #4
  5. 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'
    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
    KEY: j
    nil
    ---
    ---
    Text changed!
    The answer!
    Joel VanderWerf, Aug 18, 2004
    #5
  6. Brian Palmer

    Brian Palmer Guest


    > 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.
    Brian Palmer, Aug 18, 2004
    #6
  7. 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 :) 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.
    Joel VanderWerf, Aug 18, 2004
    #7
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Beatrice Rutger
    Replies:
    0
    Views:
    719
    Beatrice Rutger
    Jun 5, 2005
  2. Replies:
    6
    Views:
    742
    Chris Uppal
    Feb 13, 2006
  3. mem

    Observer pattern

    mem, May 18, 2004, in forum: C++
    Replies:
    2
    Views:
    534
    adiian
    Jun 4, 2007
  4. Paolino

    An observer pattern application.

    Paolino, Aug 18, 2005, in forum: Python
    Replies:
    0
    Views:
    339
    Paolino
    Aug 18, 2005
  5. Gianni Mariani

    Generalized "observer" pattern

    Gianni Mariani, Sep 23, 2007, in forum: C++
    Replies:
    5
    Views:
    598
    werasm
    Sep 24, 2007
Loading...

Share This Page