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 inheritHash
. Inheritance ceased in Rails 5.1.ActiveSupport::HashWithIndifferentAccess
still inheritsHash
.
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.