开发者

Ruby 1.8.7: intercepting chained methods for object

开发者 https://www.devze.com 2023-03-31 19:30 出处:网络
I have a class that is wrapping cells of arbitrary data; sort of a filter.The cells live in a backend datastore. but that should be as transparent as possible.

I have a class that is wrapping cells of arbitrary data; sort of a filter. The cells live in a backend datastore. but that should be as transparent as possible.

Writing straightforward accessors is simple enough:

def foo
  # fetch backend cell value and return it
end
def foo=(val)
  # store val in backend cell
end

The part I'm finding tricky is intercepting and tracking methods that would ordinarily affect the data if it weren't wrapped. For instance, if the data is an array, obj.foo << 17 would add an element to the array in situ. I want to maintain that behaviour on the data stored in the backend (i.e., obj.foo << 17 results in the stored value having an element added as well). I thought perhaps a method_missing would help:

def method_missing(meth, *args)
  methsym = meth.to_sym
  curval = self.get
  lastval = curval.clone
  opresult = curval.__send__(methsym, *args)
  if (curval != lastval)
    self.set(curval)
  end
  return opresult
end

but in combination with the reader accessor, control of the operation has moved beyond me because the thing it returns is not the thing itself. (I.e., if the backend data is an array, I'm returning a copy of it, and it's the copy that's being modified and never sent开发者_如何学C back to me.)

Is this possible? If so, how can I do it? (It's probably painfully obvious and I'm just missing it because I'm tired -- or maybe not. :-)

Thanks!

[edited]

To put it another way.. #method_missing allows you to hook into the invocation process for unknown methods. I'm looking for a way to hook into the invocation process similarly, but for all methods, known and unknown.

Thanks!


You'd need to wrap each object returned by your class inside a meta object which is aware of the backend, and could update it as needed.

In your example, you'd need to return an array wrapper object which could handle inserts, deletes, etc.

--- Edit ---

Instead of creating lots of wrapper classes, you may be able to add a 'singleton method' to the returned objects, especially if you can easily identify the methods that might need special handling.

module BackEndIF
  alias :old_send :__send__
  def __send__ method, *args
    if MethodsThatNeedSpecialHandling.include?(method)
       doSpecialHandling()
    else
      old_send(method,args)
    end
  end
end

#in your class:
def foo
   data = Backend.fetch(query)
   data.extend(BackEndIF)
   return data
end

I don't think anything based on method-missing will work, since the objects you are returning do have the methods in question. (i.e. Array does have an operator<<, it's not missing)

Or, maybe you can do something with a method_missing like the one you outline. Create a single meta_object something like this:

class DBobject
   def initialize(value, db_reference)
      @value = value
      @ref = db_reference
    end
   def method_missing(meth, *args)
     old_val = @value
     result = @value.__send__(meth, *args)
     DatabaseUpdate(@ref, @value) if (@value != old_val)
     return result   
   end
end

Then foo returns a DBObject.new(objectFromDB, referenceToDB).


I solved this by borrowing from the Delegator module. (Code that follows is not guaranteed to work; I've edited out some details by hand. But it should supply the gist.)

  • On a fetch (reader accessor), annotate the value to be passed back with modified methods:

    def enwrap(target)
      #
      # Shamelessly cadged from delegator.rb
      #
      eigenklass = eval('class << target ; self ; end')
      preserved = ::Kernel.public_instance_methods(false)
      preserved -= [ 'to_s', 'to_a', 'inspect', '==', '=~', '===' ]
      swbd = {}
      target.instance_variable_set(:@_method_map, swbd)
      target.instance_variable_set(:@_datatype, target.class)
      for t in self.class.ancestors
        preserved |= t.public_instance_methods(false)
        preserved |= t.private_instance_methods(false)
        preserved |= t.protected_instance_methods(false)
      end
      preserved << 'singleton_method_added'
      target.methods.each do |method|
        next if (preserved.include?(method))
        swbd[method] = target.method(method.to_sym)
        target.instance_eval(<<-EOS)
          def #{method}(*args, &block)
            iniself = self.clone
            result = @_method_map['#{method}'].call(*args, &block)
            if (self != iniself)
              #
              # Store the changed entity
              #
              newklass = self.class
              iniklass = iniself.instance_variable_get(:@_datatype)
              unless (self.kind_of?(iniklass))
                begin
                  raise RuntimeError('Class mismatch')
                rescue RuntimeError
                  if ($@)
                    $@.delete_if { |s|
                      %r"\A#{Regexp.quote(__FILE__)}:\d+:in `" =~ s
                    }
                  end
                  raise
                end
              end
              # update back end here
            end
            return result
          end
        EOS
      end
    end                         # End of def enwrap
    
  • On a store (writer accessor), strip the singleton methods we added:

    def unwrap(target)
      remap = target.instance_variable_get(:@_method_map)
      return nil unless (remap.kind_of?(Hash))
      remap.keys.each do |method|
        begin
          eval("class << target ; remove_method(:#{method}) ; end")
        rescue
        end
      end
      target.instance_variable_set(:@_method_map, nil)
      target.instance_variable_set(:@_datatype, nil)
    end                        # End of def unwrap
    

So when the value is requested, it gets 'wrapper' methods added to it before being returned, and the singletons are removed before anything is stored in the back end. Any operations that change the value will also update the back end as a side-effect.

There are some unfortunate side-side-effects of this technique as currently implemented. Assume that the class with the wrapped variables is instantiated in backend, and that one of the variables is accessed via ivar_foo:

backend.ivar_foo
=> nil
backend.ivar_foo = [1, 2, 3]
=> [1,2,3]
bar = backend.ivar_foo
=> [1,2,3]
bar << 4
=> [1,2,3,4]
backend.ivar_foo = 'string'
=> "string"
bar
=> [1,2,3,4]
backend.ivar_foo
=> "string"
bar.pop
=> 4
bar
=> [1,2,3]
backend.ivar_foo
=> [1,2,3]

But that's more of a curiousity than a problem for me at the moment. :-)

Thanks for the help and suggestions!

0

精彩评论

暂无评论...
验证码 换一张
取 消