如何正确使用 Ruby DelegateClass 来包装 YAML::Store?

How do I properly use the Ruby DelegateClass to wrap YAML::Store?

测试环境

我在 Ruby 3.0.2 和 Ruby 2.73 中都试过了,结果相似。对于手头的问题应该无关紧要,因为我也在不同的 shell 和 ruby 管理器下尝试过这个,但这主要是在

下测试的

描述问题(后续部分中的代码和错误)

我正在尝试使用 Delegator class 中糟糕的(甚至可能没有记录的)DelegateClass 来为 YAML::Store 创建一个允许我任意读写的外观进出 YAML 存储的密钥。但是,我显然不明白如何正确地委托给 YAML::Store 实例,或者以我想要的方式覆盖或扩展功能。

为简单起见,我将示例编写为自执行 Ruby 文件名 example.rb,因此请滚动到末尾以查看对 classes 的实际调用。我希望我的错误是微不足道的,但如果我从根本上误解了如何实际执行 CollaboratorWithData#write 和 ollaboratorWithData#read 到 MultiWriter 的委托,请教育我。

注意:我知道如何通过简单地将 YAML::Store 视为在我的 class 中实例化的对象或什至作为继承自 [= 的单独对象来解决此问题60=](例如 class MultiWriter < YAML::Store),但我非常想了解如何正确使用 Forwardable、SimpleDelegator 和 Delegate 在一般情况下和这个特定用例中包装对象。

自执行代码文件(需要一些垂直滚动)

#!/usr/bin/env ruby

require 'delegate'
require 'yaml/store'

module ExampleDelegator
  attr_accessor :yaml_store, :data

  class CollaboratorWithData
    def initialize
      @yaml_store = MultiWriter.new
      @data = {}
    end

    def some_data
      {a: 1, b:2, c: [1, 2, 3]}
    end
  end

  class MultiWriter < DelegateClass(YAML::Store)
    attr_reader :store

    def initialize file_name="store.yml", thread_safe=true
      super
      @store = self
    end

    def write **kwargs
      @store.transaction { kwargs.each { |k, v| @store[k] = v } }
    end

    def read *keys
      @store.transaction(read_only=true) { keys.map { |k| @store[k] } }
    end
  end
end

if __FILE__ == [=12=]
  include ExampleDelegator

  c = CollaboratorWithData.new
  c.data = c.some_data
  c.write(c.data)
end

运行 文件

时出错
初始化器错误
Traceback (most recent call last):
    5: from ./example.rb:40:in `<main>'
    4: from ./example.rb:40:in `new'
    3: from ./example.rb:11:in `initialize'
    2: from ./example.rb:11:in `new'
    1: from ./example.rb:24:in `initialize'
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/delegate.rb:71:in `initialize': wrong number of arguments (given 2, expected 1) (ArgumentError)

请注意,如果您仔细查看 YAML::Store#new 的调用,其中一个可能的签名 确实 有两个参数。我不明白为什么它不允许我在 IRB 中指定线程安全:

foo = YAML::Store.new 'foo.yml', true
#=> #<Psych::Store:0x00007f9f520f52c8 @opt={}, @filename="foo.yml", @abort=false, @ultra_safe=false, @thread_safe=true, @lock=#<Thread::Mutex:0x00007f9f520f5138>>
使用修改后的初始化程序的方法委托出错

即使我去掉了thread_safe参数,在调用委托的#write方法时我仍然从CollaboratorWithData得到NoM​​ethodError,这让我相信有我在初始值设定项之外执行委派的问题。

./example.rb
Traceback (most recent call last):
./example.rb:42:in `<main>': undefined method `write' for #<ExampleDelegator::CollaboratorWithData:0x00007ff86e0e15c8> (NoMethodError)

复习关于委派的一些问题:

  • 可转发: 此模块在扩展时允许您指定应委托给指定对象(通常是实例变量)的特定方法示例:
require 'forwardable'
class A 
  attr_reader :obj
  extend Forwardable 
  def_delegator :@obj, :<<
  
  def initialize(val) 
    @obj = val
  end 
end 

a = A.new([])
a << 1
#=> [1]
a.obj 
#=> [1]
  • Delegator:继承此 class 允许将方法委托给通过 new 传递的对象。该对象在实例化时使用 __setobj__ 方法内部化,调用通过 method_missing 转发;但是这里需要注意的是,它使用了 2 个默认未定义的方法(__setobj____getobj__),您需要在继承 class

    中定义它们
  • SimpleDelegator:继承自Delegator,在大多数方面都具有相同的个性;但是它预定义了前面讨论的 2 种方法。通常从 SimpleDelegator 继承优于直接从 Delegator class 继承,因为它易于使用。

  • DelegateClass:与SimpleDelegator类似,但它创建了一个新的匿名class,它定义了class 作为参数传入,然后您的继承 class 直接从这个匿名 class 继承,这允许方法通过继承而不是使用 method_missing 进行委托。例如

class A < Delegator;end
class B < DelegateClass(String);end
A.ancestors 
#=> [A, Delegator, #<Module:0x00007fffcc1cc9c8>, BasicObject]
B.ancestors
#=> [B, #<Class:0x00007fffcc5603c8>, Delegator, #<Module:0x00007fffcc1cc9c8>, BasicObject]
A.instance_methods - Object.instance_methods
#=> [:__getobj__, :__setobj__, :method_missing, :marshal_dump, :marshal_load]
B.instance_methods - Object.instance_methods
#=> Array of all the public the instance methods of String plus the above 

当您希望使用特定类型的对象实例化 class 时,这通常更可取,因为模块注入避免了与 method_missing 一起出现的完整继承链遍历。这并不意味着您不能用另一个对象实例化此 class(但是,如果您这样做并且该对象定义了传递给 DelegateClass 的 class 参数中未定义的方法,它将回退到默认 method_missing 行为)

现在输入您的代码 您的第一个错误是对 super 的调用。 Delegator 需要 1 个参数,其中 Object 的一个实例被委托给(YAML::Store 在你的情况下)但是你已经定义了 2 个参数(这两个参数都不代表你希望委托给的对象) 并且当您调用 super 时,这两个参数都会被转发,因此会出现错误。

删除 thread_safe 有效,因为您现在只有一个参数,但在这种情况下您的委托对象实际上是字符串 "store.yml"

例如,以下修改应该有效(类似于 Source Code

中提供的示例
module ExampleDelegator
  class MultiWriter < DelegateClass(YAML::Store)
    attr_reader :store

    def initialize file_name="store.yml", thread_safe=true
      @store = YAML::Store.new(file_name,thread_safe)
      super(@store)
    end
  end
end

您的第二个问题是您在 CollaboratorWithData 对象上调用 write,该对象未定义此方法且未以其他方式委派。

虽然我不是 100% 确定你的意图是什么,因为模块主体中还有一些其他奇怪的东西,比如 attr_accessor 然后被包含在全局 space 中我没有看到 Delegator 的真正原因,因为您可以通过将 yaml_store 实例变量设置为 YAML::Store 的实例(正如您已经提到的)直接使用对象,但是您如果您愿意,可以按如下方式重写代码以使用委托

require 'delegate'
require 'yaml/store'
require 'forwardable'

module ExampleDelegator
  class CollaboratorWithData
    extend Forwardable
    def_delegators :@yaml_store, :read, :write

    attr_reader :yaml_store
    attr_accessor :data
    
    def initialize(file_name="store.yml", thread_safe=true)
      @yaml_store = MultiWriter.new(YAML::Store.new(file_name,thread_safe))
      @data = {}
    end
    
    def some_data
      {a: 1, b:2, c: [1, 2, 3]}
    end
  end

  class MultiWriter < DelegateClass(YAML::Store)
    def write **kwargs
      transaction { kwargs.each { |k, v| @store[k] = v } }
    end
    def read *keys
      transaction(read_only=true) { keys.map { |k| @store[k] } }
    end
  end
end