Home > database >  Maintaining same class using delegation in Ruby
Maintaining same class using delegation in Ruby

Time:04-16

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!

CodePudding user response:

I think that if Peeps#select will return an Array, then it is OK to include Enumerable. But, you want Peeps#select to return a Peeps. I don't think you should include Enumerable. It's misleading to claim to be an Enumerable if you don't conform to its interface. This is just my opinion. There is no clear consensus on this in the ecosystem. See "Examples from the ecosystem" below.

If we accept that we cannot include Enumerable, here's the first implementation that comes to my mind.

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

I don't normally use method_missing, but it seemed convenient.

Examples from the ecosystem

There doesn't seem to be a consensus on how strictly to follow interfaces.

  • ActionController::Parameters used to inherit Hash. Inheritance ceased in Rails 5.1.
  • ActiveSupport::HashWithIndifferentAccess still inherits Hash.

CodePudding user response:

As mentioned in the other answer, this isn't really proper usage of Enumerable. That said, you could still include Enumerable and use some meta-programming to override the methods that you want to be 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

Just so you know, this really has nothing to do with inheritance vs delegation. If Peeps extended Array, you would have the exact same issue, and the exact solution above would still work.

  • Related