Hi all,

I was playing around with the observer library and thought I'd
reimplement somthing I had done in Java a while back. It uses the
observer pattern but automatically adds interceptors to every
attribute setter method - once on the first include and then
dynamically as more setter methods are added. If anybody has some
comments or hints on how to implement this in a better way, I'd be
delighted to hear them.

You can do this:

require 'state_observer'

# Model to be watched for attribute changes
class Model
  attr_accessor :name

  include StateObserver

  def id
    return @id
  end

  def id= nid
    @id = nid
  end
end

# Observer
class Watcher
    def update( name, value )
      p "#{name} set to #{value}"
    end
end

# change some stuff
m = Model.new
m.add_observer Watcher.new
m.id='000'
m.name='test'
m.id='000'

# add an attribute
class Model
  attr_accessor :new_attribute
end

# change the atttribute
m.new_attribute="new"



Output:

"id set to 000"
"name set to test"
"new_attribute set to new"



StateObserver is implemented thus:

require 'observer'
module StateObserver
  include Observable

  # hacky bits - see RDoc for define_method for explanation
  def StateObserver.create_method(target, name, &block)
     target.send(:define_method, name, &block)
   end

   def StateObserver.store_method(target, new_name, old_name)
     target.send(:alias_method, new_name, old_name)
   end
   # end hacky bits

  def StateObserver.method_interceptor_block
     lambda do |target, method_name|
      # intercept new methods ending with '='
      if method_name.to_s =~ /[a-zA-Z0-9_]=$/
        # alias this method to allow method redefinition
        return if @skip
        # prevent hooking a setter twice, in case it is redefined in a
subclass or similar
        return if respond_to? "__#{method_name}"
        @skip = true
        # alias the method
        store_method( target, "__#{method_name}", method_name)
        create_method(target, method_name) do |arg|
          # save current value
          attr_name = method_name.to_s.chop
          old = send(attr_name)
          # call original method to set new value
          self.send("__#{method_name}", arg)
          # set observer changed flag if value is different
          changed if arg != old
          # call the observer hook
          notify_observers( attr_name, arg )
        end
        @skip = nil
      end
      end
  end

  def StateObserver.included( othermod )
    # intercept existing setter methods
    othermod.public_instance_methods.each do |method_name|
      StateObserver.method_interceptor_block.call othermod, method_name
    end

    # intercept setters defined in the future
    create_method(othermod.class, :method_added) do |method_name|
      StateObserver.method_interceptor_block.call othermod, method_name
    end
  end
end


Cheers,
Max