【问题标题】:How do I properly use the Ruby DelegateClass to wrap YAML::Store?如何正确使用 Ruby DelegateClass 来包装 YAML::Store?
【发布时间】:2021-11-21 11:35:54
【问题描述】:

测试环境

我在 Ruby 3.0.2 和 Ruby 2.73 中都试过这个,结果相似。手头的问题应该没关系,因为我也在不同的 shell 和 ruby​​ 管理器下尝试过,但这主要是在以下环境下进行测试:

  • 鱼,版本 3.3.1
  • chruby:0.3.9
  • chruby-fish: 0.8.2
  • macOS 11.6

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

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

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

注意:我知道如何通过简单地将 YAML::Store 视为在我的类中实例化的对象,甚至作为从 YAML::Store 继承的单独对象(例如 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__ == $0
  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 得到 NoMethodError,这让我相信我的委托实现在初始化程序之外存在问题.

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

【问题讨论】:

    标签: ruby yaml delegates wrapper facade


    【解决方案1】:

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

    • 可转发: 此模块在扩展时允许您指定应委托给指定对象(通常是实例变量)的特定方法示例:
    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:从此类继承允许将方法委托给通过new 传递的对象。该对象在使用__setobj__ 方法实例化时被内部化,调用通过method_missing 转发;但是这里需要注意的是,它使用了 2 个默认未定义的方法(__setobj____getobj__),您需要在继承类中定义它们

    • SimpleDelegator:继承自Delegator,并在大多数方面共享其个性;但是它预定义了前面讨论的 2 种方法。通常从SimpleDelegator 继承优于直接从Delegator 类继承,因为它易于使用。

    • DelegateClass:类似于SimpleDelegator,但是它创建了一个新的匿名类,该类定义了作为参数传入的类的所有实例方法,然后您的继承类直接继承自这个匿名类它允许通过继承而不是使用 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 
    

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

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

    删除 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,然后被包含在全局空间中,就目前而言,我看不出真正的原因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
    

    【讨论】:

    • 谢谢。我赞成彻底的答案,但我需要多考虑一下。你是对的,它可能不是一个理想的用例。我最终只是简单地将 YAML::Store 子类化为具有添加的行为,这是为了简化显式调用 YAML::Store 的多个分配(或 PStore,实际上,因为它们的工作方式几乎相同)。我想要从 CollaboratorWithData 委托 #read 和 #write,并在 MultiWriter 中处理读取和写入,但正如您正确指出的那样,这并不是委托或转发的最佳用例。跨度>
    • @ToddA.Jacobs 您所描述的是Forwardable 的一个合理用例,此外还有委托创建基本上是相应更新的嵌套装饰器模式答案。
    猜你喜欢
    • 2012-10-17
    • 2016-04-15
    • 2016-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-23
    • 2017-06-15
    • 1970-01-01
    相关资源
    最近更新 更多