Ruby 1.8.7:拦截对象的链式方法

发布于 2024-12-01 21:21:30 字数 1056 浏览 1 评论 0原文

我有一个类,它包装任意数据的单元格;有点像过滤器。这些单元位于后端数据存储中。但这应该尽可能透明。

编写简单的访问器非常简单:

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

我发现棘手的部分是拦截和跟踪方法,如果数据未被包装,这些方法通常会影响数据。例如,如果数据是数组,则 obj.foo << 17 会将一个元素添加到数组中原位。我想在后端存储的数据上维护这种行为(ieobj.foo << 17 导致存储的值也添加了一个元素) 。我想也许 method_missing 会有所帮助:

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

但是与 reader 访问器结合使用时,对操作的控制已经超出了我的范围,因为它返回的东西不是东西本身。 (,如果后端数据是一个数组,我将返回它的副本,并且该副本正在被修改并且永远不会发送回给我。

)这可能吗?如果是这样,我该怎么办? (这可能是非常明显的,我只是想念它,因为我累了——或者也许不累。:-)

谢谢!

[编辑]

换句话说..#method_missing 允许您挂钩未知方法的调用过程。我正在寻找一种类似地挂钩调用过程的方法,但对于所有方法,已知未知。

谢谢!

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

坚持沉默 2024-12-08 21:21:30

您需要将类返回的每个对象包装在一个元对象中,该元对象了解后端,并可以根据需要更新它。

在您的示例中,您需要返回一个可以处理插入、删除等的数组包装对象。

--- 编辑 ---

您可以添加一个“单例方法”,而不是创建大量包装类。返回的对象,特别是如果您可以轻松识别可能需要特殊处理的方法。

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

我认为任何基于方法缺失的方法都不会起作用,因为您返回的对象确实具有相关的方法。 (即数组确实有一个运算符<<,它没有丢失)

或者,也许您可​​以使用 method_missing 做一些事情,就像您概述的那样。
创建一个像这样的元对象:

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

然后 foo 返回一个 DBObject.new(objectFromDB, referenceToDB)

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).

白衬杉格子梦 2024-12-08 21:21:30

我通过借用 Delegator 模块解决了这个问题。 (下面的代码保证工作;我已经手动编辑了一些细节。但它应该提供要点。)

  • 在获取(读取器访问器)时,将值注释为使用修改后的方法传回:

    def enwrap(目标)
      #
      # 无耻地从 delegator.rb 讨价还价
      #
      eigenklass = eval('class << target ; self ; end')
      保留 = ::Kernel.public_instance_methods(false)
      保留 -= [ 'to_s', 'to_a', '检查', '==', '=~', '===' ]
      开关={}
      target.instance_variable_set(:@_method_map, swbd)
      target.instance_variable_set(:@_datatype, target.class)
      对于 self.class.ancestors 中的 t
        保留 |= t.public_instance_methods(false)
        保留 |= t.private_instance_methods(false)
        保留 |= t.protected_instance_methods(false)
      结尾
      保存<< '单例方法添加'
      target.methods.each 执行 |method|
        接下来 if (preserved.include?(method))
        swbd[方法] = target.method(method.to_sym)
        target.instance_eval(<<-EOS)
          def #{方法}(*args, &块)
            iniself = self.克隆
            结果 = @_method_map['#{method}'].call(*args, &block)
            if (self != iniself)
              #
              # 存储改变后的实体
              #
              newklass = self.class
              iniklass = iniself.instance_variable_get(:@_datatype)
              除非 (self.kind_of?(iniklass))
                开始
                  引发 RuntimeError('类不匹配')
                救援运行时错误
                  如果($@)
                    [电子邮件受保护]_if { |s|
                      %r"\A#{Regexp.quote(__FILE__)}:\d+:in `" =~ s
                    }
                  结尾
                  增加
                结尾
              结尾
              # 在这里更新后端
            结尾
            返回结果
          结尾
        EOS
      结尾
    end # def enwrap 结束
    
  • 在存储(写入器访问器)上,剥离我们添加的单例方法:

    def unwrap(目标)
      重新映射 = target.instance_variable_get(:@_method_map)
      返回 nil 除非 (remap.kind_of?(Hash))
      remap.keys.each 执行|方法|
        开始
          eval("类 << 目标 ; remove_method(:#{method}) ; end")
        救援
        结尾
      结尾
      target.instance_variable_set(:@_method_map, nil)
      target.instance_variable_set(:@_datatype, nil)
    end # def 展开结束
    

因此,当请求该值时,它会在返回之前添加“包装”方法,并且在执行任何操作之前删除单例存储在后端。任何更改该值的操作也会作为副作用更新后端。

目前实施的这项技术一些不幸的副作用。假设带有包装变量的类在 backend 中实例化,并且通过 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]

但这更多的是目前对我来说好奇心比问题更重要。 :-)

感谢您的帮助和建议!

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 ($@)
                    [email protected]_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!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文