rubyinheritancedelegation

Maintaining same class using delegation in Ruby


I'm trying to wrap my head around delegation vs. inheritance so I'm manually delegating a version of Array. One of the specific reasons I read to do this is because when you use things like enumerables, your returned value on the inherited methods reverts back to the parent class (i.e. Array). So I did this:

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

When I instantiate this, my include and exclude function great and return a Peeps class but when using an enumerable like select, it returns Array, which prevents me from chaining further Peeps specific methods after the select. This is exactly what I'm trying to avoid with learning about delegation.

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

returns Array

If I override select, wrapping it in Peeps.new(), I get a "SystemStackError: stack level too deep". It seems to be recursively burying the list deeper into the list during the select enumeration.

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

Any help and THANKS!


Solution

  • I would recommend using both Forwardable and Enumerable. Use Forwardable to delegate the each method to your list (to satisfy the Enumerable interface requirement), and also forward any Array methods you might want to include that are not part of the Enumerable module, such as size. I would also suggest not overriding the behavior of select as it is supposed to return an array and would at the very least lead to confusion. I would suggest something like the subset provided below to implement the behavior you are looking for.

    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
    

    Example usage:

    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
    

    produces:

    Peeps
    a
    b
    c
    3
    2