在 Ruby 中使用委派维持相同的 class

Maintaining same class using delegation in Ruby

我正在努力思考委托与继承的关系,因此我手动委托了一个版本的 Array。我读到这样做的具体原因之一是因为当您使用枚举之类的东西时,继承方法上的 returned 值会恢复到父级 class(即数组)。所以我这样做了:

module PeepData
  # A list of Peeps
  class Peeps
    include Enumerable

    def initialize(list = [])
      @list = list
    end

    def [](index)
      @list[index]
    end

    def each(...)
      @list.each(...)
    end

    def reverse
      Peeps.new(@list.reverse)
    end

    def last
      @list.last
    end

    def join(...)
      @list.join(...)
    end

    def from_csv(csv_table)
      @list = []
      csv_table.each { |row| @list << Peep.new(row.to_h) }
    end

    def include(field, value)
      Peeps.new(select { |row| row[field] == value })
    end

    def exclude(field, value)
      Peeps.new(select { |row| row[field] != value })
    end

    def count_by_field(field)
      result = {}
      @list.each do |row|
        result[row[field]] = result[row[field]].to_i + 1
      end
      result
    end

    protected

    attr_reader :list
  end
end

当我实例化它时,我的包含和排除函数很好,return 一个 Peeps class 但是当使用像 select 这样的可枚举时,它 returns 数组,这阻止我在 select 之后链接更多的 Peeps 特定方法。这正是我在学习委托时试图避免的。

p = Peeps.new
p.from_csv(csv_generated_array_of_hashes)
p.select(&:certified?).class

returns 数组

如果我覆盖 select,将其包装在 Peeps.new() 中,我会收到“SystemStackError:堆栈级别太深”。它似乎在 select 枚举期间递归地将列表更深地埋入列表。

def select(...)
  Peeps.new(@list.select(...))
end

任何帮助和感谢!

我觉得如果Peeps#select会return一个Array,那么include Enumerable就可以了。但是,你想要 Peeps#select 到 return 一个 Peeps。我认为你不应该 include Enumerable。如果您不符合它的界面,那么 声称 Enumerable 是一种误导。这只是我的看法。生态系统对此没有明确的共识。请参阅下面的“来自生态系统的示例”。

如果我们承认我们不能 include Enumerable,这是我想到的第一个实现。

require 'minitest/autorun'

class Peeps
  ARRAY_METHODS = %i[flat_map map reject select]
  ELEMENT_METHODS = %i[first include? last]

  def initialize(list)
    @list = list
  end

  def inspect
    @list.join(', ')
  end

  def method_missing(mth, *args, &block)
    if ARRAY_METHODS.include?(mth)
      self.class.new(@list.send(mth, *args, &block))
    elsif ELEMENT_METHODS.include?(mth)
      @list.send(mth, *args, &block)
    else
      super
    end
  end
end

class PeepsTest < Minitest::Test
  def test_first
    assert_equal('alice', Peeps.new(%w[alice bob charlie]).first)
  end

  def test_include?
    assert Peeps.new(%w[alice bob charlie]).include?('bob')
  end

  def test_select
    peeps = Peeps.new(%w[alice bob charlie]).select { |i| i < 'c' }
    assert_instance_of(Peeps, peeps)
    assert_equal('alice, bob', peeps.inspect)
  end
end

我平时不怎么用method_missing,不过好像很方便。

来自生态系统的示例

关于如何严格遵循接口似乎没有达成共识。

  • ActionController::Parameters用于继承Hash。继承在 Rails 5.1.
  • 中停止
  • ActiveSupport::HashWithIndifferentAccess仍然继承Hash.

如另一个答案中所述,这并不是 Enumerable 的正确用法。也就是说,您仍然可以包含 Enumerable 并使用一些 meta-programming 来覆盖您想要的方法 peep-chainable:

module PeepData
  class Peeps
    include Enumerable
 
    PEEP_CHAINABLES = [:map, :select]

    PEEP_CHAINABLES.each do |method_name|
      define_method(method_name) do |&block|
        self.class.new(super(&block))
      end
    end

    # solution for select without meta-programming looks like this:
    # def select
    #   Peeps.new(super)
    # end
  end
end

如你所知,这与继承与委派无关。如果 Peeps 扩展 Array,您将遇到完全相同的问题,并且上面的确切解决方案仍然有效。

我建议使用您可能想要包含的两种 Forwardable and Enumerable. Use Forwardable to delegate the each method to your list (to satisfy the Enumerable interface requirement), and also forward any Array 方法,它们不是 Enumerable 模块的一部分,例如 size。我还建议 不要 覆盖 select 的行为,因为它应该 return 一个数组并且至少会导致混淆。我建议使用下面提供的类似 subset 的方法来实现您正在寻找的行为。

require 'forwardable'

class Peeps
  include Enumerable
  extend Forwardable

  def_delegators :@list, :each, :size

  def initialize(list = [])
    @list = list
  end

  def subset(&block)
    selected = @list.select(&block)
    Peeps.new(selected)
  end
  
  protected
  attr_reader :list

end

用法示例:

peeps = Peeps.new([:a,:b,:c])
subset = peeps.subset {|s| s != :b}
puts subset.class 
peeps.each do |peep|
   puts peep
end
puts peeps.size
puts subset.size

产生:

Peeps
a
b
c
3
2